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
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).disabledColor,
return ColoredBox(
color: Theme.of(context).colorScheme.errorContainer,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Padding(
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Icon(
Icons.cloud_off,
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> {
@override
void initState() {
super.initState();
initializeDateFormatting();
}
bool _isDownloadPending = false;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
onWillPop: () async {
Navigator.of(context)
.pop(BlocProvider.of<DocumentDetailsCubit>(context).state.document);
return Future.value(false);
return false;
},
child: DefaultTabController(
length: 3,
@@ -325,7 +319,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
_separator(),
_DetailsItem.text(
DateFormat().format(document.created),
DateFormat.yMMMd().format(document.created),
context: context,
label: S.of(context).documentCreatedPropertyLabel,
),

View File

@@ -66,7 +66,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) {
try {
_documentsCubit.load();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
builder: (context, connectivityState) {
return Scaffold(
@@ -81,40 +85,47 @@ class _DocumentsPageState extends State<DocumentsPage> {
final appliedFiltersCount = state.filter.appliedFiltersCount;
return Badge(
toAnimate: false,
animationType: BadgeAnimationType.fade,
showBadge: appliedFiltersCount > 0,
badgeContent: appliedFiltersCount > 0
? Text(state.filter.appliedFiltersCount.toString())
? Text(
state.filter.appliedFiltersCount.toString(),
style: const TextStyle(color: Colors.white),
)
: null,
child: FloatingActionButton(
child: const Icon(Icons.filter_alt),
child: const Icon(Icons.filter_alt_rounded),
onPressed: _openDocumentFilter,
),
);
},
),
resizeToAvoidBottomInset: true,
body: _buildBody(connectivityState));
body: _buildBody(connectivityState),
);
},
);
}
void _openDocumentFilter() async {
final filter = await showModalBottomSheet(
final filter = await showModalBottomSheet<DocumentFilter>(
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(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16),
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) {
final isConnected = connectivityState == ConnectivityState.connected;
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return BlocBuilder<DocumentsCubit, DocumentsState>(
@@ -143,8 +155,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
);
break;
@@ -154,8 +165,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
);
break;
@@ -175,6 +185,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
return RefreshIndicator(
onRefresh: _onRefresh,
notificationPredicate: (_) => isConnected,
child: CustomScrollView(
slivers: [
BlocListener<SavedViewCubit, SavedViewState>(
@@ -198,6 +209,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
},
child: DocumentsPageAppBar(
isOffline:
connectivityState != ConnectivityState.connected,
actions: [
const SortDocumentsButton(),
IconButton(

View File

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

View File

@@ -16,10 +16,11 @@ enum DateRangeSelection { before, after }
class DocumentFilterPanel extends StatefulWidget {
final DocumentFilter initialFilter;
final ScrollController scrollController;
const DocumentFilterPanel({
Key? key,
required this.initialFilter,
required this.scrollController,
}) : super(key: key);
@override
@@ -36,55 +37,49 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
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
Widget build(BuildContext context) {
const radius = Radius.circular(16);
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: radius,
topRight: radius,
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
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,
body: FormBuilder(
key: _formKey,
child: Column(
children: [
_buildDraggableResetHeader(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: ListView(
controller: widget.scrollController,
children: [
Text(
S.of(context).documentFilterTitle,
style: Theme.of(context).textTheme.titleLarge,
style: Theme.of(context).textTheme.headlineSmall,
).paddedOnly(
top: 16.0,
left: 16.0,
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),
@@ -101,17 +96,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
_buildTagsFormField()
.paddedSymmetrically(horizontal: 8, vertical: 4.0),
_buildTagsFormField().padded(),
],
).paddedOnly(bottom: 16),
),
),
),
],
),
),
),
);
}
@@ -128,29 +117,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Stack _buildDraggableResetHeader() {
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 {
void _resetFilter() async {
FocusScope.of(context).unfocus();
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() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
@@ -416,22 +387,23 @@ 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 {
_formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) {
final v = _formKey.currentState!.value;
DocumentFilter newFilter = DocumentFilter(
DocumentFilter newFilter = _assembleFilter();
try {
FocusScope.of(context).unfocus();
Navigator.pop(context, newFilter);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
DocumentFilter _assembleFilter() {
final v = _formKey.currentState!.value;
return DocumentFilter(
createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
correspondent: v[fkCorrespondent] as CorrespondentQuery? ??
@@ -440,8 +412,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
DocumentFilter.initial.documentType,
storagePath: v[fkStoragePath] as StoragePathQuery? ??
DocumentFilter.initial.storagePath,
tags: v[DocumentModel.tagsKey] as TagsQuery? ??
DocumentFilter.initial.tags,
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,
@@ -452,32 +424,17 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
);
try {
FocusScope.of(context).unfocus();
Navigator.pop(context, newFilter);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
void _patchFromFilter(DocumentFilter f) {
_formKey.currentState?.patchValue({
fkCorrespondent: f.correspondent,
fkDocumentType: f.documentType,
fkQuery: f.queryText,
fkStoragePath: f.storagePath,
DocumentModel.tagsKey: f.tags,
DocumentModel.titleKey: f.queryText,
QueryTypeFormField.fkQueryType: f.queryType,
fkCreatedAt: _dateTimeRangeOfNullable(
f.createdDateAfter,
f.createdDateBefore,
),
fkAddedAt: _dateTimeRangeOfNullable(
f.addedDateAfter,
f.addedDateBefore,
),
});
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_bloc/flutter_bloc.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/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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
final List<Widget> actions;
final bool isOffline;
const DocumentsPageAppBar({
super.key,
required this.isOffline,
this.actions = const [],
});
@override
@@ -21,19 +25,27 @@ class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
}
class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
static const _flexibleAreaHeight = kToolbarHeight + 48.0;
@override
Widget build(BuildContext context) {
const savedViewWidgetHeight = 48.0;
final flexibleAreaHeight = kToolbarHeight -
16 +
savedViewWidgetHeight +
(widget.isOffline ? 24 : 0);
return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, documentsState) {
final hasSelection = documentsState.selection.isNotEmpty;
if (hasSelection) {
return SliverAppBar(
expandedHeight: kToolbarHeight + _flexibleAreaHeight,
expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true,
floating: true,
pinned: true,
flexibleSpace: _buildFlexibleArea(false, documentsState.filter),
flexibleSpace: _buildFlexibleArea(
false,
documentsState.filter,
savedViewWidgetHeight,
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
@@ -50,13 +62,14 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
);
} else {
return SliverAppBar(
expandedHeight: kToolbarHeight + _flexibleAreaHeight,
expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true,
floating: true,
pinned: true,
flexibleSpace: _buildFlexibleArea(
true,
documentsState.filter,
savedViewWidgetHeight,
),
title: Text(
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})',
@@ -70,21 +83,23 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
);
}
Widget _buildFlexibleArea(bool enabled, DocumentFilter filter) {
Widget _buildFlexibleArea(
bool enabled,
DocumentFilter filter,
double savedViewHeight,
) {
return FlexibleSpaceBar(
background: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
background: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.isOffline) const OfflineBanner(),
SavedViewSelectionWidget(
height: 48,
height: savedViewHeight,
enabled: enabled,
currentFilter: filter,
),
).paddedSymmetrically(horizontal: 8.0),
],
),
),
);
}
@@ -92,8 +107,7 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) =>
BulkDeleteConfirmationDialog(state: documentsState),
) ??
BulkDeleteConfirmationDialog(state: documentsState)) ??
false;
if (shouldDelete) {
try {

View File

@@ -26,7 +26,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
@override
void initState() {
super.initState();
@@ -35,17 +35,13 @@ class _HomePageState extends State<HomePage> {
@override
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
listenWhen: (previous, current) => current == ConnectivityState.connected,
listener: (context, state) {
_initializeData(context);
},
builder: (context, connectivityState) {
return Scaffold(
appBar: connectivityState == ConnectivityState.connected
? null
: const OfflineBanner(),
child: Scaffold(
key: rootScaffoldKey,
bottomNavigationBar: BottomNavBar(
selectedIndex: _currentIndex,
@@ -71,7 +67,7 @@ class _HomePageState extends State<HomePage> {
child: const DocumentsPage(),
),
BlocProvider.value(
value: DocumentScannerCubit(),
value: _scannerCubit,
child: const ScannerPage(),
),
BlocProvider.value(
@@ -79,8 +75,7 @@ class _HomePageState extends State<HomePage> {
child: const LabelsPage(),
),
][_currentIndex],
);
},
),
);
}

View File

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

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/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_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
@@ -39,7 +41,9 @@ class _LabelsPageState extends State<LabelsPage>
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) {
return Scaffold(
drawer: const InfoDrawer(),
appBar: AppBar(
title: Text(
@@ -62,8 +66,12 @@ class _LabelsPageState extends State<LabelsPage>
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: ColoredBox(
preferredSize: Size.fromHeight(
kToolbarHeight + (!connectedState.isConnected ? 16 : 0)),
child: Column(
children: [
if (!connectedState.isConnected) const OfflineBanner(),
ColoredBox(
color: Theme.of(context).bottomAppBarColor,
child: TabBar(
indicatorColor: Theme.of(context).colorScheme.primary,
@@ -72,30 +80,40 @@ class _LabelsPageState extends State<LabelsPage>
Tab(
icon: Icon(
Icons.person_outline,
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.description_outlined,
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.label_outline,
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.folder_open,
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
)
],
),
),
],
),
),
),
body: TabBarView(
@@ -103,7 +121,8 @@ class _LabelsPageState extends State<LabelsPage>
children: [
BlocProvider(
create: (context) => LabelCubit(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
RepositoryProvider.of<LabelRepository<Correspondent>>(
context),
),
child: LabelTabView<Correspondent>(
filterBuilder: (label) => DocumentFilter(
@@ -111,8 +130,9 @@ class _LabelsPageState extends State<LabelsPage>
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel:
S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageCorrespondentEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageCorrespondentEmptyStateDescriptionText,
@@ -121,7 +141,8 @@ class _LabelsPageState extends State<LabelsPage>
),
BlocProvider(
create: (context) => LabelCubit(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
RepositoryProvider.of<LabelRepository<DocumentType>>(
context),
),
child: LabelTabView<DocumentType>(
filterBuilder: (label) => DocumentFilter(
@@ -129,8 +150,9 @@ class _LabelsPageState extends State<LabelsPage>
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel:
S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageDocumentTypeEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageDocumentTypeEmptyStateDescriptionText,
@@ -166,7 +188,8 @@ class _LabelsPageState extends State<LabelsPage>
),
BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
RepositoryProvider.of<LabelRepository<StoragePath>>(
context),
),
child: LabelTabView<StoragePath>(
onEdit: _openEditStoragePathPage,
@@ -175,8 +198,9 @@ class _LabelsPageState extends State<LabelsPage>
pageSize: label.documentCount ?? 0,
),
contentBuilder: (path) => Text(path.path ?? ""),
emptyStateActionButtonLabel:
S.of(context).labelsPageStoragePathEmptyStateAddNewLabel,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageStoragePathEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageStoragePathEmptyStateDescriptionText,
@@ -185,6 +209,8 @@ class _LabelsPageState extends State<LabelsPage>
),
],
),
);
},
),
);
}

View File

@@ -9,11 +9,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.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/repository/label_repository.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/store/local_vault.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.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/view/document_upload_preparation_page.dart';
@@ -38,23 +40,28 @@ class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) {
return Scaffold(
drawer: const InfoDrawer(),
floatingActionButton: FloatingActionButton(
onPressed: () => _openDocumentScanner(context),
child: const Icon(Icons.add_a_photo_outlined),
),
appBar: _buildAppBar(context),
appBar: _buildAppBar(context, connectedState.isConnected),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: _buildBody(),
child: _buildBody(connectedState.isConnected),
),
);
},
);
}
AppBar _buildAppBar(BuildContext context) {
AppBar _buildAppBar(BuildContext context, bool isConnected) {
return AppBar(
title: Text(S.of(context).documentScannerPageTitle),
bottom: !isConnected ? const OfflineBanner() : null,
actions: [
BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
@@ -86,7 +93,7 @@ class _ScannerPageState extends State<ScannerPage>
BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
return IconButton(
onPressed: state.isEmpty
onPressed: state.isEmpty || !isConnected
? null
: () => _onPrepareDocumentUpload(context),
icon: const Icon(Icons.done),
@@ -127,7 +134,7 @@ class _ScannerPageState extends State<ScannerPage>
BlocProvider.of<DocumentScannerCubit>(context).state,
);
final bytes = await doc.save();
Navigator.of(context).push(
final uploaded = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
@@ -152,10 +159,14 @@ class _ScannerPageState extends State<ScannerPage>
),
),
),
);
) ??
false;
if (uploaded) {
BlocProvider.of<DocumentScannerCubit>(context).reset();
}
}
Widget _buildBody() {
Widget _buildBody(bool isConnected) {
return BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, scans) {
if (scans.isNotEmpty) {
@@ -181,7 +192,7 @@ class _ScannerPageState extends State<ScannerPage>
child: Text(S
.of(context)
.documentScannerPageUploadFromThisDeviceButtonLabel),
onPressed: _onUploadFromFilesystem,
onPressed: isConnected ? _onUploadFromFilesystem : null,
),
],
),
@@ -195,7 +206,7 @@ class _ScannerPageState extends State<ScannerPage>
return GridView.builder(
itemCount: scans.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisCount: 3,
childAspectRatio: 1 / sqrt(2),
crossAxisSpacing: 10,
mainAxisSpacing: 10,

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:photo_view/photo_view.dart';
typedef DeleteCallback = void Function();
@@ -28,7 +29,6 @@ class GridImageItemWidget extends StatefulWidget {
}
class _GridImageItemWidgetState extends State<GridImageItemWidget> {
bool isProcessing = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -37,51 +37,72 @@ class _GridImageItemWidgetState extends State<GridImageItemWidget> {
);
}
Card _buildImageItem(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
Widget _buildImageItem(BuildContext context) {
final borderRadius = BorderRadius.circular(12);
return ClipRRect(
child: Card(
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack(
clipBehavior: Clip.antiAliasWithSaveLayer,
children: [
Align(
alignment: Alignment.topCenter,
child: ClipRRect(
borderRadius: borderRadius,
child: SizedBox(
height: 100,
child: Stack(
children: [
Align(alignment: Alignment.bottomCenter, child: _buildNumbering()),
Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: widget.onDelete,
icon: const Icon(Icons.close),
),
),
isProcessing
? _buildIsProcessing()
: Align(
SizedBox(
width: double.infinity,
height: 100,
child: FittedBox(
fit: BoxFit.fill,
clipBehavior: Clip.antiAliasWithSaveLayer,
alignment: Alignment.center,
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.file(
widget.file,
fit: BoxFit.contain,
),
),
),
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,
),
),
),
],
),
),
);
}
Center _buildIsProcessing() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
CircularProgressIndicator(),
Text(
"Processing transformation...",
textAlign: TextAlign.center,
),
),
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: widget.onDelete,
child: Text("Remove"),
),
),
],
),
),
);
}
@@ -90,17 +111,12 @@ class _GridImageItemWidgetState extends State<GridImageItemWidget> {
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: _buildNumbering(prefix: "Image"),
title: Text(
"${S.of(context).scannerPageImagePreviewTitle} ${widget.index + 1}/${widget.totalNumberOfFiles}"),
),
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": {},
"genericActionUploadLabel": "Nahrát",
"@genericActionUploadLabel": {},
"genericMessageOfflineText": "Jste offline. Ověřte připojení.",
"genericMessageOfflineText": "Jste offline.",
"@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.",
"@inboxPageDocumentRemovedMessageText": {},
@@ -408,6 +408,8 @@
"@savedViewShowOnDashboardLabel": {},
"savedViewsLabel": "Uložené náhledy",
"@savedViewsLabel": {},
"scannerPageImagePreviewTitle": "",
"@scannerPageImagePreviewTitle": {},
"serverInformationPaperlessVersionText": "Verze Paperless serveru",
"@serverInformationPaperlessVersionText": {},
"settingsPageAppearanceSettingDarkThemeLabel": "Tmavý vzhled",

View File

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

View File

@@ -290,7 +290,7 @@
"@genericActionUpdateLabel": {},
"genericActionUploadLabel": "Upload",
"@genericActionUploadLabel": {},
"genericMessageOfflineText": "You're offline. Check your connection.",
"genericMessageOfflineText": "You're offline.",
"@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Document removed from inbox.",
"@inboxPageDocumentRemovedMessageText": {},
@@ -408,6 +408,8 @@
"@savedViewShowOnDashboardLabel": {},
"savedViewsLabel": "Saved Views",
"@savedViewsLabel": {},
"scannerPageImagePreviewTitle": "Scan",
"@scannerPageImagePreviewTitle": {},
"serverInformationPaperlessVersionText": "Paperless server version",
"@serverInformationPaperlessVersionText": {},
"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:fluttertoast/fluttertoast.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_standalone.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/saved_view_repository.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.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/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -114,8 +116,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
theme: ThemeData(
brightness: Brightness.light,
useMaterial3: true,
colorScheme:
ColorScheme.fromSeed(seedColor: Colors.lightGreen).copyWith(),
colorSchemeSeed: Colors.lightGreen,
appBarTheme: const AppBarTheme(
scrolledUnderElevation: 0.0,
),
@@ -123,7 +124,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: EdgeInsets.symmetric(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
@@ -143,7 +144,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: EdgeInsets.symmetric(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
@@ -195,7 +196,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
}
late final SharedMediaFile file;
if (Platform.isIOS) {
// Workaround: https://stackoverflow.com/a/72813212
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
file = SharedMediaFile(
files.first.path.replaceAll('file://', ''),
files.first.thumbnail,
@@ -221,8 +222,22 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
final success = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: getIt<DocumentScannerCubit>(),
builder: (context) => BlocProvider(
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(
fileBytes: bytes,
filename: filename,
@@ -244,6 +259,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override
void initState() {
super.initState();
initializeDateFormatting();
// For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles);
// For sharing files coming from outside the app while the app is closed
@@ -312,12 +328,12 @@ class BiometricAuthenticationPage extends StatelessWidget {
ElevatedButton(
onPressed: () =>
BlocProvider.of<AuthenticationCubit>(context).logout(),
child: Text("Log out"),
child: const Text("Log out"),
),
ElevatedButton(
onPressed: () => BlocProvider.of<AuthenticationCubit>(context)
.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>();
late PackageInfo kPackageInfo;
class SnackBarActionConfig {
final String label;
final VoidCallback onPressed;
SnackBarActionConfig({
required this.label,
required this.onPressed,
});
}
void showSnackBar(
BuildContext context,
String message, {
String? details,
SnackBarAction? action,
SnackBarActionConfig? action,
}) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
@@ -29,7 +39,13 @@ void showSnackBar(
content: Text(
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),
),
);
@@ -43,9 +59,8 @@ void showGenericError(
showSnackBar(
context,
error.toString(),
action: SnackBarAction(
action: SnackBarActionConfig(
label: S.of(context).errorReportLabel,
textColor: Colors.amber,
onPressed: () => GithubIssueService.createIssueFromError(
context,
stackTrace: stackTrace,
@@ -69,14 +84,6 @@ void showErrorMessage(
context,
translateError(context, error.code),
details: error.details,
action: SnackBarAction(
label: S.of(context).errorReportLabel,
textColor: Colors.amber,
onPressed: () => GithubIssueService.createIssueFromError(
context,
stackTrace: stackTrace,
),
),
);
log(
"An error has occurred.",