mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 22:07:49 -06:00
Bugfixes, finished filter rework
This commit is contained in:
@@ -37,4 +37,10 @@ class ConnectivityCubit extends Cubit<ConnectivityState> {
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnectivityState { connected, notConnected, undefined }
|
||||
enum ConnectivityState {
|
||||
connected,
|
||||
notConnected,
|
||||
undefined;
|
||||
|
||||
bool get isConnected => this == connected;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -66,55 +66,66 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
previous != ConnectivityState.connected &&
|
||||
current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
_documentsCubit.load();
|
||||
try {
|
||||
_documentsCubit.load();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
return Scaffold(
|
||||
drawer: BlocProvider.value(
|
||||
value: BlocProvider.of<AuthenticationCubit>(context),
|
||||
child: InfoDrawer(
|
||||
afterInboxClosed: () => _documentsCubit.reload(),
|
||||
),
|
||||
drawer: BlocProvider.value(
|
||||
value: BlocProvider.of<AuthenticationCubit>(context),
|
||||
child: InfoDrawer(
|
||||
afterInboxClosed: () => _documentsCubit.reload(),
|
||||
),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
return Badge(
|
||||
toAnimate: false,
|
||||
showBadge: appliedFiltersCount > 0,
|
||||
badgeContent: appliedFiltersCount > 0
|
||||
? Text(state.filter.appliedFiltersCount.toString())
|
||||
: null,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt),
|
||||
onPressed: _openDocumentFilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: _buildBody(connectivityState));
|
||||
),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
return Badge(
|
||||
toAnimate: false,
|
||||
animationType: BadgeAnimationType.fade,
|
||||
showBadge: appliedFiltersCount > 0,
|
||||
badgeContent: appliedFiltersCount > 0
|
||||
? Text(
|
||||
state.filter.appliedFiltersCount.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)
|
||||
: null,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_rounded),
|
||||
onPressed: _openDocumentFilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,80 +37,68 @@ 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(
|
||||
child: ListView(
|
||||
controller: widget.scrollController,
|
||||
children: [
|
||||
_buildDraggableResetHeader(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).documentFilterTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).documentFilterTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddedOnly(
|
||||
top: 16.0,
|
||||
left: 16.0,
|
||||
bottom: 24,
|
||||
),
|
||||
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() {
|
||||
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,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 {
|
||||
_formKey.currentState?.save();
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final v = _formKey.currentState!.value;
|
||||
DocumentFilter newFilter = DocumentFilter(
|
||||
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,
|
||||
);
|
||||
DocumentFilter newFilter = _assembleFilter();
|
||||
try {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context, newFilter);
|
||||
@@ -461,23 +401,40 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
});
|
||||
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? ??
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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,30 +83,31 @@ 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(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SavedViewSelectionWidget(
|
||||
height: 48,
|
||||
enabled: enabled,
|
||||
currentFilter: filter,
|
||||
),
|
||||
],
|
||||
),
|
||||
background: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (widget.isOffline) const OfflineBanner(),
|
||||
SavedViewSelectionWidget(
|
||||
height: savedViewHeight,
|
||||
enabled: enabled,
|
||||
currentFilter: filter,
|
||||
).paddedSymmetrically(horizontal: 8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
BulkDeleteConfirmationDialog(state: documentsState),
|
||||
) ??
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
BulkDeleteConfirmationDialog(state: documentsState)) ??
|
||||
false;
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
|
||||
@@ -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,52 +35,47 @@ 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(),
|
||||
key: rootScaffoldKey,
|
||||
bottomNavigationBar: BottomNavBar(
|
||||
selectedIndex: _currentIndex,
|
||||
onNavigationChanged: (index) {
|
||||
if (_currentIndex != index) {
|
||||
setState(() => _currentIndex = index);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
key: rootScaffoldKey,
|
||||
bottomNavigationBar: BottomNavBar(
|
||||
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(),
|
||||
body: [
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SavedViewCubit(
|
||||
RepositoryProvider.of<SavedViewRepository>(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const DocumentsPage(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: DocumentScannerCubit(),
|
||||
child: const ScannerPage(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
|
||||
child: const LabelsPage(),
|
||||
),
|
||||
][_currentIndex],
|
||||
);
|
||||
},
|
||||
BlocProvider.value(
|
||||
value: _scannerCubit,
|
||||
child: const ScannerPage(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
|
||||
child: const LabelsPage(),
|
||||
),
|
||||
][_currentIndex],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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,152 +41,176 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
drawer: const InfoDrawer(),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
[
|
||||
S.of(context).labelsPageCorrespondentsTitleText,
|
||||
S.of(context).labelsPageDocumentTypesTitleText,
|
||||
S.of(context).labelsPageTagsTitleText,
|
||||
S.of(context).labelsPageStoragePathTitleText
|
||||
][_currentIndex],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: [
|
||||
_openAddCorrespondentPage,
|
||||
_openAddDocumentTypePage,
|
||||
_openAddTagPage,
|
||||
_openAddStoragePathPage,
|
||||
][_currentIndex],
|
||||
icon: const Icon(Icons.add),
|
||||
)
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).bottomAppBarColor,
|
||||
child: TabBar(
|
||||
indicatorColor: Theme.of(context).colorScheme.primary,
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return Scaffold(
|
||||
drawer: const InfoDrawer(),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
[
|
||||
S.of(context).labelsPageCorrespondentsTitleText,
|
||||
S.of(context).labelsPageDocumentTypesTitleText,
|
||||
S.of(context).labelsPageTagsTitleText,
|
||||
S.of(context).labelsPageStoragePathTitleText
|
||||
][_currentIndex],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: [
|
||||
_openAddCorrespondentPage,
|
||||
_openAddDocumentTypePage,
|
||||
_openAddTagPage,
|
||||
_openAddStoragePathPage,
|
||||
][_currentIndex],
|
||||
icon: const Icon(Icons.add),
|
||||
)
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
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,
|
||||
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(
|
||||
icon: Icon(
|
||||
Icons.description_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
child: LabelTabView<Correspondent>(
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
correspondent: CorrespondentQuery.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)
|
||||
.labelsPageCorrespondentEmptyStateAddNewLabel,
|
||||
emptyStateDescription: S
|
||||
.of(context)
|
||||
.labelsPageCorrespondentEmptyStateDescriptionText,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.label_outline,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit(
|
||||
RepositoryProvider.of<LabelRepository<DocumentType>>(
|
||||
context),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.folder_open,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
child: LabelTabView<DocumentType>(
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
documentType: DocumentTypeQuery.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
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: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)
|
||||
.labelsPageDocumentTypeEmptyStateAddNewLabel,
|
||||
emptyStateDescription: S
|
||||
.of(context)
|
||||
.labelsPageDocumentTypeEmptyStateDescriptionText,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
),
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel,
|
||||
emptyStateDescription: S
|
||||
.of(context)
|
||||
.labelsPageCorrespondentEmptyStateDescriptionText,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit(
|
||||
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
|
||||
),
|
||||
child: LabelTabView<DocumentType>(
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
documentType: DocumentTypeQuery.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Tag>(
|
||||
RepositoryProvider.of<LabelRepository<Tag>>(context),
|
||||
),
|
||||
child: LabelTabView<Tag>(
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
tags: IdsTagsQuery.fromIds([label.id!]),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
onEdit: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel,
|
||||
emptyStateDescription: S
|
||||
.of(context)
|
||||
.labelsPageDocumentTypeEmptyStateDescriptionText,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Tag>(
|
||||
RepositoryProvider.of<LabelRepository<Tag>>(context),
|
||||
),
|
||||
child: LabelTabView<Tag>(
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
tags: IdsTagsQuery.fromIds([label.id!]),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 Scaffold(
|
||||
drawer: const InfoDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _openDocumentScanner(context),
|
||||
child: const Icon(Icons.add_a_photo_outlined),
|
||||
),
|
||||
appBar: _buildAppBar(context),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildBody(),
|
||||
),
|
||||
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, connectedState.isConnected),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
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,35 +134,39 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
BlocProvider.of<DocumentScannerCubit>(context).state,
|
||||
);
|
||||
final bytes = await doc.save();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LabelRepositoriesProvider(
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentUploadCubit(
|
||||
localVault: getIt<LocalVault>(),
|
||||
documentApi: getIt<PaperlessDocumentsApi>(),
|
||||
correspondentRepository:
|
||||
RepositoryProvider.of<LabelRepository<Correspondent>>(
|
||||
context,
|
||||
final uploaded = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LabelRepositoriesProvider(
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentUploadCubit(
|
||||
localVault: getIt<LocalVault>(),
|
||||
documentApi: getIt<PaperlessDocumentsApi>(),
|
||||
correspondentRepository:
|
||||
RepositoryProvider.of<LabelRepository<Correspondent>>(
|
||||
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>>(
|
||||
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,
|
||||
|
||||
@@ -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,70 +37,86 @@ 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.bottomCenter, child: _buildNumbering()),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
onPressed: widget.onDelete,
|
||||
icon: const Icon(Icons.close),
|
||||
alignment: Alignment.topCenter,
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
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) {
|
||||
Navigator.of(context).push(
|
||||
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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user