Bugfixes, finished filter rework

This commit is contained in:
Anton Stubenbord
2022-12-14 00:53:42 +01:00
parent 3dc590baa2
commit f001059401
20 changed files with 566 additions and 804 deletions

View File

@@ -37,4 +37,10 @@ class ConnectivityCubit extends Cubit<ConnectivityState> {
} }
} }
enum ConnectivityState { connected, notConnected, undefined } enum ConnectivityState {
connected,
notConnected,
undefined;
bool get isConnected => this == connected;
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class ComingSoon extends StatelessWidget {
const ComingSoon({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
"Coming Soon\u2122",
style: Theme.of(context).textTheme.titleLarge,
),
);
}
}

View File

@@ -1,77 +0,0 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
class ElevatedConfirmationButton extends StatefulWidget {
factory ElevatedConfirmationButton.icon(BuildContext context,
{required void Function() onPressed,
required Icon icon,
required Widget label}) {
final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
final double gap =
scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
return ElevatedConfirmationButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
),
onPressed: onPressed,
);
}
const ElevatedConfirmationButton({
Key? key,
this.color,
required this.onPressed,
required this.child,
this.confirmWidget = const Text("Confirm?"),
}) : super(key: key);
final Color? color;
final void Function()? onPressed;
final Widget child;
final Widget confirmWidget;
@override
State<ElevatedConfirmationButton> createState() =>
_ElevatedConfirmationButtonState();
}
class _ElevatedConfirmationButtonState
extends State<ElevatedConfirmationButton> {
bool _clickedOnce = false;
double? _originalWidth;
final GlobalKey _originalWidgetKey = GlobalKey();
@override
Widget build(BuildContext context) {
if (!_clickedOnce) {
return ElevatedButton(
key: _originalWidgetKey,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color),
),
onPressed: () {
_originalWidth = (_originalWidgetKey.currentContext
?.findRenderObject() as RenderBox)
.size
.width;
setState(() => _clickedOnce = true);
},
child: widget.child,
);
} else {
return Builder(builder: (context) {
return SizedBox(
width: _originalWidth,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color),
),
onPressed: widget.onPressed,
child: widget.confirmWidget,
),
);
});
}
}
}

View File

@@ -1,215 +0,0 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
@immutable
class ExpandableFloatingActionButton extends StatefulWidget {
const ExpandableFloatingActionButton({
super.key,
this.initialOpen,
required this.distance,
required this.children,
});
final bool? initialOpen;
final double distance;
final List<Widget> children;
@override
State<ExpandableFloatingActionButton> createState() =>
_ExpandableFloatingActionButtonState();
}
class _ExpandableFloatingActionButtonState
extends State<ExpandableFloatingActionButton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _expandAnimation;
bool _open = false;
@override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
vsync: this,
);
_expandAnimation = CurvedAnimation(
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.easeOutQuad,
parent: _controller,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_open = !_open;
if (_open) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
_buildTapToCloseFab(),
..._buildExpandingActionButtons(),
_buildTapToOpenFab(),
],
),
);
}
Widget _buildTapToCloseFab() {
return SizedBox(
width: 56.0,
height: 56.0,
child: Center(
child: Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
elevation: 4.0,
child: InkWell(
onTap: _toggle,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.close,
color: Theme.of(context).primaryColor,
),
),
),
),
),
);
}
List<Widget> _buildExpandingActionButtons() {
final children = <Widget>[];
final count = widget.children.length;
final step = 90.0 / (count - 1);
for (var i = 0, angleInDegrees = 0.0;
i < count;
i++, angleInDegrees += step) {
children.add(
_ExpandingActionButton(
directionInDegrees: angleInDegrees,
maxDistance: widget.distance,
progress: _expandAnimation,
child: widget.children[i],
),
);
}
return children;
}
Widget _buildTapToOpenFab() {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_open ? 0.7 : 1.0,
_open ? 0.7 : 1.0,
1.0,
),
duration: const Duration(milliseconds: 250),
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 250),
child: FloatingActionButton(
onPressed: _toggle,
child: const Icon(Icons.create),
),
),
),
);
}
}
@immutable
class _ExpandingActionButton extends StatelessWidget {
const _ExpandingActionButton({
required this.directionInDegrees,
required this.maxDistance,
required this.progress,
required this.child,
});
final double directionInDegrees;
final double maxDistance;
final Animation<double> progress;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: progress,
builder: (context, child) {
final offset = Offset.fromDirection(
directionInDegrees * (math.pi / 180.0),
progress.value * maxDistance,
);
return Positioned(
right: 4.0 + offset.dx,
bottom: 4.0 + offset.dy,
child: Transform.rotate(
angle: (1.0 - progress.value) * math.pi / 2,
child: child!,
),
);
},
child: FadeTransition(
opacity: progress,
child: child,
),
);
}
}
@immutable
class ExpandableActionButton extends StatelessWidget {
const ExpandableActionButton({
super.key,
this.color,
this.onPressed,
required this.icon,
});
final VoidCallback? onPressed;
final Widget icon;
final Color? color;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 48,
width: 48,
child: ElevatedButton(
onPressed: onPressed,
child: icon,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.all(color),
),
),
);
}
}

View File

@@ -6,20 +6,26 @@ class OfflineBanner extends StatelessWidget with PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return ColoredBox(
color: Theme.of(context).disabledColor, color: Theme.of(context).colorScheme.errorContainer,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0), padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Icon( child: Icon(
Icons.cloud_off, Icons.cloud_off,
size: 24, size: 24,
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
Text(
S.of(context).genericMessageOfflineText,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
), ),
), ),
Text(S.of(context).genericMessageOfflineText),
], ],
), ),
); );

View File

@@ -45,21 +45,15 @@ class DocumentDetailsPage extends StatefulWidget {
} }
class _DocumentDetailsPageState extends State<DocumentDetailsPage> { class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
@override
void initState() {
super.initState();
initializeDateFormatting();
}
bool _isDownloadPending = false; bool _isDownloadPending = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () async {
Navigator.of(context) Navigator.of(context)
.pop(BlocProvider.of<DocumentDetailsCubit>(context).state.document); .pop(BlocProvider.of<DocumentDetailsCubit>(context).state.document);
return Future.value(false); return false;
}, },
child: DefaultTabController( child: DefaultTabController(
length: 3, length: 3,
@@ -325,7 +319,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
_separator(), _separator(),
_DetailsItem.text( _DetailsItem.text(
DateFormat().format(document.created), DateFormat.yMMMd().format(document.created),
context: context, context: context,
label: S.of(context).documentCreatedPropertyLabel, label: S.of(context).documentCreatedPropertyLabel,
), ),

View File

@@ -66,55 +66,66 @@ class _DocumentsPageState extends State<DocumentsPage> {
previous != ConnectivityState.connected && previous != ConnectivityState.connected &&
current == ConnectivityState.connected, current == ConnectivityState.connected,
listener: (context, state) { listener: (context, state) {
_documentsCubit.load(); try {
_documentsCubit.load();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}, },
builder: (context, connectivityState) { builder: (context, connectivityState) {
return Scaffold( return Scaffold(
drawer: BlocProvider.value( drawer: BlocProvider.value(
value: BlocProvider.of<AuthenticationCubit>(context), value: BlocProvider.of<AuthenticationCubit>(context),
child: InfoDrawer( child: InfoDrawer(
afterInboxClosed: () => _documentsCubit.reload(), afterInboxClosed: () => _documentsCubit.reload(),
),
), ),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>( ),
builder: (context, state) { floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
final appliedFiltersCount = state.filter.appliedFiltersCount; builder: (context, state) {
return Badge( final appliedFiltersCount = state.filter.appliedFiltersCount;
toAnimate: false, return Badge(
showBadge: appliedFiltersCount > 0, toAnimate: false,
badgeContent: appliedFiltersCount > 0 animationType: BadgeAnimationType.fade,
? Text(state.filter.appliedFiltersCount.toString()) showBadge: appliedFiltersCount > 0,
: null, badgeContent: appliedFiltersCount > 0
child: FloatingActionButton( ? Text(
child: const Icon(Icons.filter_alt), state.filter.appliedFiltersCount.toString(),
onPressed: _openDocumentFilter, style: const TextStyle(color: Colors.white),
), )
); : null,
}, child: FloatingActionButton(
), child: const Icon(Icons.filter_alt_rounded),
resizeToAvoidBottomInset: true, onPressed: _openDocumentFilter,
body: _buildBody(connectivityState)); ),
);
},
),
resizeToAvoidBottomInset: true,
body: _buildBody(connectivityState),
);
}, },
); );
} }
void _openDocumentFilter() async { void _openDocumentFilter() async {
final filter = await showModalBottomSheet( final filter = await showModalBottomSheet<DocumentFilter>(
context: context, context: context,
builder: (context) => SizedBox(
height: MediaQuery.of(context).size.height - kToolbarHeight - 16,
child: LabelsBlocProvider(
child: DocumentFilterPanel(
initialFilter: _documentsCubit.state.filter,
),
),
),
isDismissible: true,
isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0), topLeft: Radius.circular(16),
topRight: Radius.circular(16.0), topRight: Radius.circular(16),
),
),
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: .9,
builder: (context, controller) => LabelsBlocProvider(
child: DocumentFilterPanel(
initialFilter: _documentsCubit.state.filter,
scrollController: controller,
),
), ),
), ),
); );
@@ -125,6 +136,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
} }
Widget _buildBody(ConnectivityState connectivityState) { Widget _buildBody(ConnectivityState connectivityState) {
final isConnected = connectivityState == ConnectivityState.connected;
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>( return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) { builder: (context, settings) {
return BlocBuilder<DocumentsCubit, DocumentsState>( return BlocBuilder<DocumentsCubit, DocumentsState>(
@@ -143,8 +155,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state, state: state,
onSelected: _onSelected, onSelected: _onSelected,
pagingController: _pagingController, pagingController: _pagingController,
hasInternetConnection: hasInternetConnection: isConnected,
connectivityState == ConnectivityState.connected,
onTagSelected: _addTagToFilter, onTagSelected: _addTagToFilter,
); );
break; break;
@@ -154,8 +165,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state, state: state,
onSelected: _onSelected, onSelected: _onSelected,
pagingController: _pagingController, pagingController: _pagingController,
hasInternetConnection: hasInternetConnection: isConnected,
connectivityState == ConnectivityState.connected,
onTagSelected: _addTagToFilter, onTagSelected: _addTagToFilter,
); );
break; break;
@@ -175,6 +185,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
return RefreshIndicator( return RefreshIndicator(
onRefresh: _onRefresh, onRefresh: _onRefresh,
notificationPredicate: (_) => isConnected,
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
BlocListener<SavedViewCubit, SavedViewState>( BlocListener<SavedViewCubit, SavedViewState>(
@@ -198,6 +209,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
} }
}, },
child: DocumentsPageAppBar( child: DocumentsPageAppBar(
isOffline:
connectivityState != ConnectivityState.connected,
actions: [ actions: [
const SortDocumentsButton(), const SortDocumentsButton(),
IconButton( IconButton(

View File

@@ -41,6 +41,7 @@ class DocumentGridItem extends StatelessWidget {
? Theme.of(context).colorScheme.inversePrimary ? Theme.of(context).colorScheme.inversePrimary
: Theme.of(context).cardColor, : Theme.of(context).cardColor,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: 1, aspectRatio: 1,
@@ -74,8 +75,9 @@ class DocumentGridItem extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Text( Text(
DateFormat.yMMMd(Intl.getCurrentLocale()) DateFormat.yMMMd().format(
.format(document.created), document.created,
),
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
), ),
], ],

View File

@@ -16,10 +16,11 @@ enum DateRangeSelection { before, after }
class DocumentFilterPanel extends StatefulWidget { class DocumentFilterPanel extends StatefulWidget {
final DocumentFilter initialFilter; final DocumentFilter initialFilter;
final ScrollController scrollController;
const DocumentFilterPanel({ const DocumentFilterPanel({
Key? key, Key? key,
required this.initialFilter, required this.initialFilter,
required this.scrollController,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -36,80 +37,68 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
if (start == null && end == null) {
return null;
}
if (start != null && end != null) {
return DateTimeRange(start: start, end: end);
}
assert(start != null || end != null);
final singleDate = (start ?? end)!;
return DateTimeRange(start: singleDate, end: singleDate);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const radius = Radius.circular(16);
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: radius, topLeft: Radius.circular(16),
topRight: radius, topRight: Radius.circular(16),
), ),
child: Scaffold( child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
icon: const Icon(Icons.done),
label: Text(S.of(context).documentFilterApplyFilterLabel),
onPressed: _onApplyFilter,
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextButton.icon(
onPressed: _resetFilter,
icon: const Icon(Icons.refresh),
label: Text(S.of(context).documentFilterResetLabel),
)
],
),
),
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
body: FormBuilder( body: FormBuilder(
key: _formKey, key: _formKey,
child: Column( child: ListView(
controller: widget.scrollController,
children: [ children: [
_buildDraggableResetHeader(), Text(
Row( S.of(context).documentFilterTitle,
mainAxisAlignment: MainAxisAlignment.spaceBetween, style: Theme.of(context).textTheme.headlineSmall,
children: [ ).paddedOnly(
Text( top: 16.0,
S.of(context).documentFilterTitle, left: 16.0,
style: Theme.of(context).textTheme.titleLarge, bottom: 24,
),
TextButton(
onPressed: _onApplyFilter,
child: Text(S.of(context).documentFilterApplyFilterLabel),
),
],
).padded(),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: SingleChildScrollView(
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(S.of(context).documentFilterSearchLabel),
).paddedOnly(left: 8.0),
_buildQueryFormField().padded(),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentFilterAdvancedLabel,
),
).padded(),
_buildCreatedDateRangePickerFormField(),
_buildAddedDateRangePickerFormField(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
_buildTagsFormField()
.paddedSymmetrically(horizontal: 8, vertical: 4.0),
],
).paddedOnly(bottom: 16),
),
),
), ),
Align(
alignment: Alignment.centerLeft,
child: Text(S.of(context).documentFilterSearchLabel),
).paddedOnly(left: 8.0),
_buildQueryFormField().padded(),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentFilterAdvancedLabel,
),
).padded(),
_buildCreatedDateRangePickerFormField(),
_buildAddedDateRangePickerFormField(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
_buildTagsFormField().padded(),
], ],
), ).paddedOnly(bottom: 16),
), ),
), ),
); );
@@ -128,29 +117,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Stack _buildDraggableResetHeader() { void _resetFilter() async {
return Stack(
alignment: Alignment.center,
children: [
_buildDragLine(),
Align(
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label: Text(S.of(context).documentFilterResetLabel),
onPressed: () => _resetFilter(context),
),
),
],
);
}
void _resetFilter(BuildContext context) async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
Navigator.pop(context, DocumentFilter.initial); Navigator.pop(context, DocumentFilter.initial);
} }
//TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField!
Widget _buildDocumentTypeFormField() { Widget _buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>( return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) { builder: (context, state) {
@@ -416,42 +387,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildDragLine() {
return Container(
width: 48,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
);
}
void _onApplyFilter() async { void _onApplyFilter() async {
_formKey.currentState?.save(); _formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) { if (_formKey.currentState?.validate() ?? false) {
final v = _formKey.currentState!.value; final v = _formKey.currentState!.value;
DocumentFilter newFilter = DocumentFilter( DocumentFilter newFilter = _assembleFilter();
createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
correspondent: v[fkCorrespondent] as CorrespondentQuery? ??
DocumentFilter.initial.correspondent,
documentType: v[fkDocumentType] as DocumentTypeQuery? ??
DocumentFilter.initial.documentType,
storagePath: v[fkStoragePath] as StoragePathQuery? ??
DocumentFilter.initial.storagePath,
tags: v[DocumentModel.tagsKey] as TagsQuery? ??
DocumentFilter.initial.tags,
queryText: v[fkQuery] as String?,
addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end,
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
asnQuery: widget.initialFilter.asnQuery,
page: 1,
pageSize: widget.initialFilter.pageSize,
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
);
try { try {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
Navigator.pop(context, newFilter); Navigator.pop(context, newFilter);
@@ -461,23 +401,40 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
} }
} }
void _patchFromFilter(DocumentFilter f) { DocumentFilter _assembleFilter() {
_formKey.currentState?.patchValue({ final v = _formKey.currentState!.value;
fkCorrespondent: f.correspondent, return DocumentFilter(
fkDocumentType: f.documentType, createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
fkQuery: f.queryText, createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
fkStoragePath: f.storagePath, correspondent: v[fkCorrespondent] as CorrespondentQuery? ??
DocumentModel.tagsKey: f.tags, DocumentFilter.initial.correspondent,
DocumentModel.titleKey: f.queryText, documentType: v[fkDocumentType] as DocumentTypeQuery? ??
QueryTypeFormField.fkQueryType: f.queryType, DocumentFilter.initial.documentType,
fkCreatedAt: _dateTimeRangeOfNullable( storagePath: v[fkStoragePath] as StoragePathQuery? ??
f.createdDateAfter, DocumentFilter.initial.storagePath,
f.createdDateBefore, tags:
), v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
fkAddedAt: _dateTimeRangeOfNullable( queryText: v[fkQuery] as String?,
f.addedDateAfter, addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end,
f.addedDateBefore, addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
), queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
}); asnQuery: widget.initialFilter.asnQuery,
page: 1,
pageSize: widget.initialFilter.pageSize,
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
);
} }
} }
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
if (start == null && end == null) {
return null;
}
if (start != null && end != null) {
return DateTimeRange(start: start, end: end);
}
assert(start != null || end != null);
final singleDate = (start ?? end)!;
return DateTimeRange(start: singleDate, end: singleDate);
}

View File

@@ -1,17 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget { class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
final List<Widget> actions; final List<Widget> actions;
final bool isOffline;
const DocumentsPageAppBar({ const DocumentsPageAppBar({
super.key, super.key,
required this.isOffline,
this.actions = const [], this.actions = const [],
}); });
@override @override
@@ -21,19 +25,27 @@ class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
} }
class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> { class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
static const _flexibleAreaHeight = kToolbarHeight + 48.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const savedViewWidgetHeight = 48.0;
final flexibleAreaHeight = kToolbarHeight -
16 +
savedViewWidgetHeight +
(widget.isOffline ? 24 : 0);
return BlocBuilder<DocumentsCubit, DocumentsState>( return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, documentsState) { builder: (context, documentsState) {
final hasSelection = documentsState.selection.isNotEmpty; final hasSelection = documentsState.selection.isNotEmpty;
if (hasSelection) { if (hasSelection) {
return SliverAppBar( return SliverAppBar(
expandedHeight: kToolbarHeight + _flexibleAreaHeight, expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true, snap: true,
floating: true, floating: true,
pinned: true, pinned: true,
flexibleSpace: _buildFlexibleArea(false, documentsState.filter), flexibleSpace: _buildFlexibleArea(
false,
documentsState.filter,
savedViewWidgetHeight,
),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => onPressed: () =>
@@ -50,13 +62,14 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
); );
} else { } else {
return SliverAppBar( return SliverAppBar(
expandedHeight: kToolbarHeight + _flexibleAreaHeight, expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true, snap: true,
floating: true, floating: true,
pinned: true, pinned: true,
flexibleSpace: _buildFlexibleArea( flexibleSpace: _buildFlexibleArea(
true, true,
documentsState.filter, documentsState.filter,
savedViewWidgetHeight,
), ),
title: Text( title: Text(
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})', '${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})',
@@ -70,30 +83,31 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
); );
} }
Widget _buildFlexibleArea(bool enabled, DocumentFilter filter) { Widget _buildFlexibleArea(
bool enabled,
DocumentFilter filter,
double savedViewHeight,
) {
return FlexibleSpaceBar( return FlexibleSpaceBar(
background: Padding( background: Column(
padding: const EdgeInsets.all(8.0), mainAxisAlignment: MainAxisAlignment.end,
child: Column( children: [
mainAxisAlignment: MainAxisAlignment.end, if (widget.isOffline) const OfflineBanner(),
children: [ SavedViewSelectionWidget(
SavedViewSelectionWidget( height: savedViewHeight,
height: 48, enabled: enabled,
enabled: enabled, currentFilter: filter,
currentFilter: filter, ).paddedSymmetrically(horizontal: 8.0),
), ],
],
),
), ),
); );
} }
void _onDelete(BuildContext context, DocumentsState documentsState) async { void _onDelete(BuildContext context, DocumentsState documentsState) async {
final shouldDelete = await showDialog<bool>( final shouldDelete = await showDialog<bool>(
context: context, context: context,
builder: (context) => builder: (context) =>
BulkDeleteConfirmationDialog(state: documentsState), BulkDeleteConfirmationDialog(state: documentsState)) ??
) ??
false; false;
if (shouldDelete) { if (shouldDelete) {
try { try {

View File

@@ -26,7 +26,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
int _currentIndex = 0; int _currentIndex = 0;
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -35,52 +35,47 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<ConnectivityCubit, ConnectivityState>( return BlocListener<ConnectivityCubit, ConnectivityState>(
//Only re-initialize data if the connectivity changed from not connected to connected //Only re-initialize data if the connectivity changed from not connected to connected
listenWhen: (previous, current) => current == ConnectivityState.connected, listenWhen: (previous, current) => current == ConnectivityState.connected,
listener: (context, state) { listener: (context, state) {
_initializeData(context); _initializeData(context);
}, },
builder: (context, connectivityState) { child: Scaffold(
return Scaffold( key: rootScaffoldKey,
appBar: connectivityState == ConnectivityState.connected bottomNavigationBar: BottomNavBar(
? null selectedIndex: _currentIndex,
: const OfflineBanner(), onNavigationChanged: (index) {
key: rootScaffoldKey, if (_currentIndex != index) {
bottomNavigationBar: BottomNavBar( setState(() => _currentIndex = index);
selectedIndex: _currentIndex, }
onNavigationChanged: (index) { },
if (_currentIndex != index) { ),
setState(() => _currentIndex = index); drawer: const InfoDrawer(),
} body: [
}, MultiBlocProvider(
providers: [
BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
),
BlocProvider(
create: (context) => SavedViewCubit(
RepositoryProvider.of<SavedViewRepository>(context),
),
),
],
child: const DocumentsPage(),
), ),
drawer: const InfoDrawer(), BlocProvider.value(
body: [ value: _scannerCubit,
MultiBlocProvider( child: const ScannerPage(),
providers: [ ),
BlocProvider.value( BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()), value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
), child: const LabelsPage(),
BlocProvider( ),
create: (context) => SavedViewCubit( ][_currentIndex],
RepositoryProvider.of<SavedViewRepository>(context), ),
),
),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: DocumentScannerCubit(),
child: const ScannerPage(),
),
BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
child: const LabelsPage(),
),
][_currentIndex],
);
},
); );
} }

View File

@@ -216,7 +216,7 @@ class _InboxPageState extends State<InboxPage> {
showSnackBar( showSnackBar(
context, context,
S.of(context).inboxPageDocumentRemovedMessageText, S.of(context).inboxPageDocumentRemovedMessageText,
action: SnackBarAction( action: SnackBarActionConfig(
label: S.of(context).inboxPageUndoRemoveText, label: S.of(context).inboxPageUndoRemoveText,
onPressed: () => _onUndoMarkAsSeen(doc, removedTags), onPressed: () => _onUndoMarkAsSeen(doc, removedTags),
), ),

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
@@ -39,152 +41,176 @@ class _LabelsPageState extends State<LabelsPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return DefaultTabController(
length: 3, length: 3,
child: Scaffold( child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
drawer: const InfoDrawer(), builder: (context, connectedState) {
appBar: AppBar( return Scaffold(
title: Text( drawer: const InfoDrawer(),
[ appBar: AppBar(
S.of(context).labelsPageCorrespondentsTitleText, title: Text(
S.of(context).labelsPageDocumentTypesTitleText, [
S.of(context).labelsPageTagsTitleText, S.of(context).labelsPageCorrespondentsTitleText,
S.of(context).labelsPageStoragePathTitleText S.of(context).labelsPageDocumentTypesTitleText,
][_currentIndex], S.of(context).labelsPageTagsTitleText,
), S.of(context).labelsPageStoragePathTitleText
actions: [ ][_currentIndex],
IconButton( ),
onPressed: [ actions: [
_openAddCorrespondentPage, IconButton(
_openAddDocumentTypePage, onPressed: [
_openAddTagPage, _openAddCorrespondentPage,
_openAddStoragePathPage, _openAddDocumentTypePage,
][_currentIndex], _openAddTagPage,
icon: const Icon(Icons.add), _openAddStoragePathPage,
) ][_currentIndex],
], icon: const Icon(Icons.add),
bottom: PreferredSize( )
preferredSize: const Size.fromHeight(kToolbarHeight), ],
child: ColoredBox( bottom: PreferredSize(
color: Theme.of(context).bottomAppBarColor, preferredSize: Size.fromHeight(
child: TabBar( kToolbarHeight + (!connectedState.isConnected ? 16 : 0)),
indicatorColor: Theme.of(context).colorScheme.primary, child: Column(
controller: _tabController, children: [
tabs: [ if (!connectedState.isConnected) const OfflineBanner(),
Tab( ColoredBox(
icon: Icon( color: Theme.of(context).bottomAppBarColor,
Icons.person_outline, child: TabBar(
color: Theme.of(context).colorScheme.onPrimaryContainer, indicatorColor: Theme.of(context).colorScheme.primary,
controller: _tabController,
tabs: [
Tab(
icon: Icon(
Icons.person_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.description_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.label_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.folder_open,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
)
],
),
), ),
],
),
),
),
body: TabBarView(
controller: _tabController,
children: [
BlocProvider(
create: (context) => LabelCubit(
RepositoryProvider.of<LabelRepository<Correspondent>>(
context),
), ),
Tab( child: LabelTabView<Correspondent>(
icon: Icon( filterBuilder: (label) => DocumentFilter(
Icons.description_outlined, correspondent: CorrespondentQuery.fromId(label.id),
color: Theme.of(context).colorScheme.onPrimaryContainer, pageSize: label.documentCount ?? 0,
), ),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageCorrespondentEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageCorrespondentEmptyStateDescriptionText,
onAddNew: _openAddCorrespondentPage,
), ),
Tab( ),
icon: Icon( BlocProvider(
Icons.label_outline, create: (context) => LabelCubit(
color: Theme.of(context).colorScheme.onPrimaryContainer, RepositoryProvider.of<LabelRepository<DocumentType>>(
), context),
), ),
Tab( child: LabelTabView<DocumentType>(
icon: Icon( filterBuilder: (label) => DocumentFilter(
Icons.folder_open, documentType: DocumentTypeQuery.fromId(label.id),
color: Theme.of(context).colorScheme.onPrimaryContainer, pageSize: label.documentCount ?? 0,
), ),
) onEdit: _openEditDocumentTypePage,
], emptyStateActionButtonLabel: S
), .of(context)
), .labelsPageDocumentTypeEmptyStateAddNewLabel,
), emptyStateDescription: S
), .of(context)
body: TabBarView( .labelsPageDocumentTypeEmptyStateDescriptionText,
controller: _tabController, onAddNew: _openAddDocumentTypePage,
children: [ ),
BlocProvider(
create: (context) => LabelCubit(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
child: LabelTabView<Correspondent>(
filterBuilder: (label) => DocumentFilter(
correspondent: CorrespondentQuery.fromId(label.id),
pageSize: label.documentCount ?? 0,
), ),
onEdit: _openEditCorrespondentPage, BlocProvider(
emptyStateActionButtonLabel: create: (context) => LabelCubit<Tag>(
S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel, RepositoryProvider.of<LabelRepository<Tag>>(context),
emptyStateDescription: S ),
.of(context) child: LabelTabView<Tag>(
.labelsPageCorrespondentEmptyStateDescriptionText, filterBuilder: (label) => DocumentFilter(
onAddNew: _openAddCorrespondentPage, tags: IdsTagsQuery.fromIds([label.id!]),
), pageSize: label.documentCount ?? 0,
), ),
BlocProvider( onEdit: _openEditTagPage,
create: (context) => LabelCubit( leadingBuilder: (t) => CircleAvatar(
RepositoryProvider.of<LabelRepository<DocumentType>>(context), backgroundColor: t.color,
), child: t.isInboxTag ?? false
child: LabelTabView<DocumentType>( ? Icon(
filterBuilder: (label) => DocumentFilter( Icons.inbox,
documentType: DocumentTypeQuery.fromId(label.id), color: t.textColor,
pageSize: label.documentCount ?? 0, )
: null,
),
contentBuilder: (t) => Text(t.match ?? ''),
emptyStateActionButtonLabel:
S.of(context).labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription:
S.of(context).labelsPageTagsEmptyStateDescriptionText,
onAddNew: _openAddTagPage,
),
), ),
onEdit: _openEditDocumentTypePage, BlocProvider(
emptyStateActionButtonLabel: create: (context) => LabelCubit<StoragePath>(
S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel, RepositoryProvider.of<LabelRepository<StoragePath>>(
emptyStateDescription: S context),
.of(context) ),
.labelsPageDocumentTypeEmptyStateDescriptionText, child: LabelTabView<StoragePath>(
onAddNew: _openAddDocumentTypePage, onEdit: _openEditStoragePathPage,
), filterBuilder: (label) => DocumentFilter(
), storagePath: StoragePathQuery.fromId(label.id),
BlocProvider( pageSize: label.documentCount ?? 0,
create: (context) => LabelCubit<Tag>( ),
RepositoryProvider.of<LabelRepository<Tag>>(context), contentBuilder: (path) => Text(path.path ?? ""),
), emptyStateActionButtonLabel: S
child: LabelTabView<Tag>( .of(context)
filterBuilder: (label) => DocumentFilter( .labelsPageStoragePathEmptyStateAddNewLabel,
tags: IdsTagsQuery.fromIds([label.id!]), emptyStateDescription: S
pageSize: label.documentCount ?? 0, .of(context)
.labelsPageStoragePathEmptyStateDescriptionText,
onAddNew: _openAddStoragePathPage,
),
), ),
onEdit: _openEditTagPage, ],
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag ?? false
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
contentBuilder: (t) => Text(t.match ?? ''),
emptyStateActionButtonLabel:
S.of(context).labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription:
S.of(context).labelsPageTagsEmptyStateDescriptionText,
onAddNew: _openAddTagPage,
),
), ),
BlocProvider( );
create: (context) => LabelCubit<StoragePath>( },
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: LabelTabView<StoragePath>(
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath: StoragePathQuery.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
contentBuilder: (path) => Text(path.path ?? ""),
emptyStateActionButtonLabel:
S.of(context).labelsPageStoragePathEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageStoragePathEmptyStateDescriptionText,
onAddNew: _openAddStoragePathPage,
),
),
],
),
), ),
); );
} }

View File

@@ -9,11 +9,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
@@ -38,23 +40,28 @@ class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
drawer: const InfoDrawer(), builder: (context, connectedState) {
floatingActionButton: FloatingActionButton( return Scaffold(
onPressed: () => _openDocumentScanner(context), drawer: const InfoDrawer(),
child: const Icon(Icons.add_a_photo_outlined), floatingActionButton: FloatingActionButton(
), onPressed: () => _openDocumentScanner(context),
appBar: _buildAppBar(context), child: const Icon(Icons.add_a_photo_outlined),
body: Padding( ),
padding: const EdgeInsets.all(8.0), appBar: _buildAppBar(context, connectedState.isConnected),
child: _buildBody(), body: Padding(
), padding: const EdgeInsets.all(8.0),
child: _buildBody(connectedState.isConnected),
),
);
},
); );
} }
AppBar _buildAppBar(BuildContext context) { AppBar _buildAppBar(BuildContext context, bool isConnected) {
return AppBar( return AppBar(
title: Text(S.of(context).documentScannerPageTitle), title: Text(S.of(context).documentScannerPageTitle),
bottom: !isConnected ? const OfflineBanner() : null,
actions: [ actions: [
BlocBuilder<DocumentScannerCubit, List<File>>( BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) { builder: (context, state) {
@@ -86,7 +93,7 @@ class _ScannerPageState extends State<ScannerPage>
BlocBuilder<DocumentScannerCubit, List<File>>( BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) { builder: (context, state) {
return IconButton( return IconButton(
onPressed: state.isEmpty onPressed: state.isEmpty || !isConnected
? null ? null
: () => _onPrepareDocumentUpload(context), : () => _onPrepareDocumentUpload(context),
icon: const Icon(Icons.done), icon: const Icon(Icons.done),
@@ -127,35 +134,39 @@ class _ScannerPageState extends State<ScannerPage>
BlocProvider.of<DocumentScannerCubit>(context).state, BlocProvider.of<DocumentScannerCubit>(context).state,
); );
final bytes = await doc.save(); final bytes = await doc.save();
Navigator.of(context).push( final uploaded = await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => LabelRepositoriesProvider( builder: (_) => LabelRepositoriesProvider(
child: BlocProvider( child: BlocProvider(
create: (context) => DocumentUploadCubit( create: (context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(), localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(), documentApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository: correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>( RepositoryProvider.of<LabelRepository<Correspondent>>(
context, context,
),
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
),
), ),
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
), ),
), ),
), ) ??
), false;
); if (uploaded) {
BlocProvider.of<DocumentScannerCubit>(context).reset();
}
} }
Widget _buildBody() { Widget _buildBody(bool isConnected) {
return BlocBuilder<DocumentScannerCubit, List<File>>( return BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, scans) { builder: (context, scans) {
if (scans.isNotEmpty) { if (scans.isNotEmpty) {
@@ -181,7 +192,7 @@ class _ScannerPageState extends State<ScannerPage>
child: Text(S child: Text(S
.of(context) .of(context)
.documentScannerPageUploadFromThisDeviceButtonLabel), .documentScannerPageUploadFromThisDeviceButtonLabel),
onPressed: _onUploadFromFilesystem, onPressed: isConnected ? _onUploadFromFilesystem : null,
), ),
], ],
), ),
@@ -195,7 +206,7 @@ class _ScannerPageState extends State<ScannerPage>
return GridView.builder( return GridView.builder(
itemCount: scans.length, itemCount: scans.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: 3,
childAspectRatio: 1 / sqrt(2), childAspectRatio: 1 / sqrt(2),
crossAxisSpacing: 10, crossAxisSpacing: 10,
mainAxisSpacing: 10, mainAxisSpacing: 10,

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
typedef DeleteCallback = void Function(); typedef DeleteCallback = void Function();
@@ -28,7 +29,6 @@ class GridImageItemWidget extends StatefulWidget {
} }
class _GridImageItemWidgetState extends State<GridImageItemWidget> { class _GridImageItemWidgetState extends State<GridImageItemWidget> {
bool isProcessing = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@@ -37,70 +37,86 @@ class _GridImageItemWidgetState extends State<GridImageItemWidget> {
); );
} }
Card _buildImageItem(BuildContext context) { Widget _buildImageItem(BuildContext context) {
return Card( final borderRadius = BorderRadius.circular(12);
child: Padding( return ClipRRect(
padding: const EdgeInsets.only(bottom: 8.0), child: Card(
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack( child: Stack(
clipBehavior: Clip.antiAliasWithSaveLayer,
children: [ children: [
Align(alignment: Alignment.bottomCenter, child: _buildNumbering()),
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topCenter,
child: IconButton( child: ClipRRect(
onPressed: widget.onDelete, borderRadius: borderRadius,
icon: const Icon(Icons.close), child: SizedBox(
height: 100,
child: Stack(
children: [
SizedBox(
width: double.infinity,
height: 100,
child: FittedBox(
fit: BoxFit.fill,
clipBehavior: Clip.antiAliasWithSaveLayer,
alignment: Alignment.center,
child: Image.file(
widget.file,
),
),
),
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(12),
),
),
child: Text(
"${widget.index + 1}/${widget.totalNumberOfFiles}",
style: Theme.of(context).textTheme.caption,
),
),
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: widget.onDelete,
child: Text("Remove"),
), ),
), ),
isProcessing
? _buildIsProcessing()
: Align(
alignment: Alignment.center,
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.file(
widget.file,
fit: BoxFit.contain,
),
),
),
], ],
), ),
), ),
); );
} }
Center _buildIsProcessing() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
CircularProgressIndicator(),
Text(
"Processing transformation...",
textAlign: TextAlign.center,
),
],
),
);
}
void _showImage(BuildContext context) { void _showImage(BuildContext context) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: AppBar( appBar: AppBar(
title: _buildNumbering(prefix: "Image"), title: Text(
"${S.of(context).scannerPageImagePreviewTitle} ${widget.index + 1}/${widget.totalNumberOfFiles}"),
), ),
body: PhotoView(imageProvider: FileImage(widget.file)), body: PhotoView(imageProvider: FileImage(widget.file)),
), ),
), ),
); );
} }
Widget _buildNumbering({String? prefix}) {
return Text(
"${prefix ?? ""} ${widget.index + 1}/${widget.totalNumberOfFiles}",
);
}
} }

View File

@@ -290,7 +290,7 @@
"@genericActionUpdateLabel": {}, "@genericActionUpdateLabel": {},
"genericActionUploadLabel": "Nahrát", "genericActionUploadLabel": "Nahrát",
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "Jste offline. Ověřte připojení.", "genericMessageOfflineText": "Jste offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.", "inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.",
"@inboxPageDocumentRemovedMessageText": {}, "@inboxPageDocumentRemovedMessageText": {},
@@ -408,6 +408,8 @@
"@savedViewShowOnDashboardLabel": {}, "@savedViewShowOnDashboardLabel": {},
"savedViewsLabel": "Uložené náhledy", "savedViewsLabel": "Uložené náhledy",
"@savedViewsLabel": {}, "@savedViewsLabel": {},
"scannerPageImagePreviewTitle": "",
"@scannerPageImagePreviewTitle": {},
"serverInformationPaperlessVersionText": "Verze Paperless serveru", "serverInformationPaperlessVersionText": "Verze Paperless serveru",
"@serverInformationPaperlessVersionText": {}, "@serverInformationPaperlessVersionText": {},
"settingsPageAppearanceSettingDarkThemeLabel": "Tmavý vzhled", "settingsPageAppearanceSettingDarkThemeLabel": "Tmavý vzhled",

View File

@@ -290,7 +290,7 @@
"@genericActionUpdateLabel": {}, "@genericActionUpdateLabel": {},
"genericActionUploadLabel": "Hochladen", "genericActionUploadLabel": "Hochladen",
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "Du bist offline. Überprüfe deine Verbindung.", "genericMessageOfflineText": "Du bist offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.", "inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.",
"@inboxPageDocumentRemovedMessageText": {}, "@inboxPageDocumentRemovedMessageText": {},
@@ -408,6 +408,8 @@
"@savedViewShowOnDashboardLabel": {}, "@savedViewShowOnDashboardLabel": {},
"savedViewsLabel": "Gespeicherte Ansichten", "savedViewsLabel": "Gespeicherte Ansichten",
"@savedViewsLabel": {}, "@savedViewsLabel": {},
"scannerPageImagePreviewTitle": "Aufnahme",
"@scannerPageImagePreviewTitle": {},
"serverInformationPaperlessVersionText": "Paperless Server-Version", "serverInformationPaperlessVersionText": "Paperless Server-Version",
"@serverInformationPaperlessVersionText": {}, "@serverInformationPaperlessVersionText": {},
"settingsPageAppearanceSettingDarkThemeLabel": "Dunkler Modus", "settingsPageAppearanceSettingDarkThemeLabel": "Dunkler Modus",

View File

@@ -290,7 +290,7 @@
"@genericActionUpdateLabel": {}, "@genericActionUpdateLabel": {},
"genericActionUploadLabel": "Upload", "genericActionUploadLabel": "Upload",
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "You're offline. Check your connection.", "genericMessageOfflineText": "You're offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Document removed from inbox.", "inboxPageDocumentRemovedMessageText": "Document removed from inbox.",
"@inboxPageDocumentRemovedMessageText": {}, "@inboxPageDocumentRemovedMessageText": {},
@@ -408,6 +408,8 @@
"@savedViewShowOnDashboardLabel": {}, "@savedViewShowOnDashboardLabel": {},
"savedViewsLabel": "Saved Views", "savedViewsLabel": "Saved Views",
"@savedViewsLabel": {}, "@savedViewsLabel": {},
"scannerPageImagePreviewTitle": "Scan",
"@scannerPageImagePreviewTitle": {},
"serverInformationPaperlessVersionText": "Paperless server version", "serverInformationPaperlessVersionText": "Paperless server version",
"@serverInformationPaperlessVersionText": {}, "@serverInformationPaperlessVersionText": {},
"settingsPageAppearanceSettingDarkThemeLabel": "Dark Theme", "settingsPageAppearanceSettingDarkThemeLabel": "Dark Theme",

View File

@@ -7,6 +7,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:intl/intl_standalone.dart'; import 'package:intl/intl_standalone.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -25,14 +26,15 @@ import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -114,8 +116,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
theme: ThemeData( theme: ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
useMaterial3: true, useMaterial3: true,
colorScheme: colorSchemeSeed: Colors.lightGreen,
ColorScheme.fromSeed(seedColor: Colors.lightGreen).copyWith(),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
scrolledUnderElevation: 0.0, scrolledUnderElevation: 0.0,
), ),
@@ -123,7 +124,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
contentPadding: EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: 16.0, vertical: 16.0,
), ),
@@ -143,7 +144,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
contentPadding: EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: 16.0, vertical: 16.0,
), ),
@@ -195,7 +196,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
} }
late final SharedMediaFile file; late final SharedMediaFile file;
if (Platform.isIOS) { if (Platform.isIOS) {
// Workaround: https://stackoverflow.com/a/72813212 // Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
file = SharedMediaFile( file = SharedMediaFile(
files.first.path.replaceAll('file://', ''), files.first.path.replaceAll('file://', ''),
files.first.thumbnail, files.first.thumbnail,
@@ -221,8 +222,22 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
final success = await Navigator.push( final success = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BlocProvider.value( builder: (context) => BlocProvider(
value: getIt<DocumentScannerCubit>(), create: (BuildContext context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
),
child: DocumentUploadPreparationPage( child: DocumentUploadPreparationPage(
fileBytes: bytes, fileBytes: bytes,
filename: filename, filename: filename,
@@ -244,6 +259,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initializeDateFormatting();
// For sharing files coming from outside the app while the app is still opened // For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles); ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles);
// For sharing files coming from outside the app while the app is closed // For sharing files coming from outside the app while the app is closed
@@ -312,12 +328,12 @@ class BiometricAuthenticationPage extends StatelessWidget {
ElevatedButton( ElevatedButton(
onPressed: () => onPressed: () =>
BlocProvider.of<AuthenticationCubit>(context).logout(), BlocProvider.of<AuthenticationCubit>(context).logout(),
child: Text("Log out"), child: const Text("Log out"),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => BlocProvider.of<AuthenticationCubit>(context) onPressed: () => BlocProvider.of<AuthenticationCubit>(context)
.restoreSessionState(), .restoreSessionState(),
child: Text("Authenticate"), child: const Text("Authenticate"),
), ),
], ],
), ),

View File

@@ -16,11 +16,21 @@ final dateFormat = DateFormat("yyyy-MM-dd");
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
late PackageInfo kPackageInfo; late PackageInfo kPackageInfo;
class SnackBarActionConfig {
final String label;
final VoidCallback onPressed;
SnackBarActionConfig({
required this.label,
required this.onPressed,
});
}
void showSnackBar( void showSnackBar(
BuildContext context, BuildContext context,
String message, { String message, {
String? details, String? details,
SnackBarAction? action, SnackBarActionConfig? action,
}) { }) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
..hideCurrentSnackBar() ..hideCurrentSnackBar()
@@ -29,7 +39,13 @@ void showSnackBar(
content: Text( content: Text(
message + (details != null ? ' ($details)' : ''), message + (details != null ? ' ($details)' : ''),
), ),
action: action, action: action != null
? SnackBarAction(
label: action.label,
onPressed: action.onPressed,
textColor: Theme.of(context).colorScheme.onInverseSurface,
)
: null,
duration: const Duration(seconds: 5), duration: const Duration(seconds: 5),
), ),
); );
@@ -43,9 +59,8 @@ void showGenericError(
showSnackBar( showSnackBar(
context, context,
error.toString(), error.toString(),
action: SnackBarAction( action: SnackBarActionConfig(
label: S.of(context).errorReportLabel, label: S.of(context).errorReportLabel,
textColor: Colors.amber,
onPressed: () => GithubIssueService.createIssueFromError( onPressed: () => GithubIssueService.createIssueFromError(
context, context,
stackTrace: stackTrace, stackTrace: stackTrace,
@@ -69,14 +84,6 @@ void showErrorMessage(
context, context,
translateError(context, error.code), translateError(context, error.code),
details: error.details, details: error.details,
action: SnackBarAction(
label: S.of(context).errorReportLabel,
textColor: Colors.amber,
onPressed: () => GithubIssueService.createIssueFromError(
context,
stackTrace: stackTrace,
),
),
); );
log( log(
"An error has occurred.", "An error has occurred.",