diff --git a/lib/core/bloc/connectivity_cubit.dart b/lib/core/bloc/connectivity_cubit.dart index 0e21828..08f7054 100644 --- a/lib/core/bloc/connectivity_cubit.dart +++ b/lib/core/bloc/connectivity_cubit.dart @@ -37,4 +37,10 @@ class ConnectivityCubit extends Cubit { } } -enum ConnectivityState { connected, notConnected, undefined } +enum ConnectivityState { + connected, + notConnected, + undefined; + + bool get isConnected => this == connected; +} diff --git a/lib/core/widgets/coming_soon_placeholder.dart b/lib/core/widgets/coming_soon_placeholder.dart deleted file mode 100644 index d8fda8a..0000000 --- a/lib/core/widgets/coming_soon_placeholder.dart +++ /dev/null @@ -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, - ), - ); - } -} diff --git a/lib/core/widgets/confirm_button.dart b/lib/core/widgets/confirm_button.dart deleted file mode 100644 index 2e0b076..0000000 --- a/lib/core/widgets/confirm_button.dart +++ /dev/null @@ -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: [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 createState() => - _ElevatedConfirmationButtonState(); -} - -class _ElevatedConfirmationButtonState - extends State { - 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, - ), - ); - }); - } - } -} diff --git a/lib/core/widgets/expandable_floating_action_button.dart b/lib/core/widgets/expandable_floating_action_button.dart deleted file mode 100644 index 3314a2e..0000000 --- a/lib/core/widgets/expandable_floating_action_button.dart +++ /dev/null @@ -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 children; - - @override - State createState() => - _ExpandableFloatingActionButtonState(); -} - -class _ExpandableFloatingActionButtonState - extends State - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _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 _buildExpandingActionButtons() { - final children = []; - 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 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), - ), - ), - ); - } -} diff --git a/lib/core/widgets/offline_banner.dart b/lib/core/widgets/offline_banner.dart index 4d78214..334be93 100644 --- a/lib/core/widgets/offline_banner.dart +++ b/lib/core/widgets/offline_banner.dart @@ -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), ], ), ); diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index be5f951..1329613 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -45,21 +45,15 @@ class DocumentDetailsPage extends StatefulWidget { } class _DocumentDetailsPageState extends State { - @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(context).state.document); - return Future.value(false); + return false; }, child: DefaultTabController( length: 3, @@ -325,7 +319,7 @@ class _DocumentDetailsPageState extends State { ), _separator(), _DetailsItem.text( - DateFormat().format(document.created), + DateFormat.yMMMd().format(document.created), context: context, label: S.of(context).documentCreatedPropertyLabel, ), diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index aa17b9f..4450591 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -66,55 +66,66 @@ class _DocumentsPageState extends State { 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(context), - child: InfoDrawer( - afterInboxClosed: () => _documentsCubit.reload(), - ), + drawer: BlocProvider.value( + value: BlocProvider.of(context), + child: InfoDrawer( + afterInboxClosed: () => _documentsCubit.reload(), ), - floatingActionButton: BlocBuilder( - 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( + 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( 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 { } Widget _buildBody(ConnectivityState connectivityState) { + final isConnected = connectivityState == ConnectivityState.connected; return BlocBuilder( builder: (context, settings) { return BlocBuilder( @@ -143,8 +155,7 @@ class _DocumentsPageState extends State { state: state, onSelected: _onSelected, pagingController: _pagingController, - hasInternetConnection: - connectivityState == ConnectivityState.connected, + hasInternetConnection: isConnected, onTagSelected: _addTagToFilter, ); break; @@ -154,8 +165,7 @@ class _DocumentsPageState extends State { state: state, onSelected: _onSelected, pagingController: _pagingController, - hasInternetConnection: - connectivityState == ConnectivityState.connected, + hasInternetConnection: isConnected, onTagSelected: _addTagToFilter, ); break; @@ -175,6 +185,7 @@ class _DocumentsPageState extends State { return RefreshIndicator( onRefresh: _onRefresh, + notificationPredicate: (_) => isConnected, child: CustomScrollView( slivers: [ BlocListener( @@ -198,6 +209,8 @@ class _DocumentsPageState extends State { } }, child: DocumentsPageAppBar( + isOffline: + connectivityState != ConnectivityState.connected, actions: [ const SortDocumentsButton(), IconButton( diff --git a/lib/features/documents/view/widgets/grid/document_grid_item.dart b/lib/features/documents/view/widgets/grid/document_grid_item.dart index 84a8956..ed34966 100644 --- a/lib/features/documents/view/widgets/grid/document_grid_item.dart +++ b/lib/features/documents/view/widgets/grid/document_grid_item.dart @@ -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, ), ], diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 73ce03c..b2f8240 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -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 { final _formKey = GlobalKey(); - 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 { ); } - 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, LabelState>( builder: (context, state) { @@ -416,42 +387,11 @@ class _DocumentFilterPanelState extends State { ); } - 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 { } } - 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); +} diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index 651b169..1b547c1 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -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 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 { - 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( 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 { ); } 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 { ); } - 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( - context: context, - builder: (context) => - BulkDeleteConfirmationDialog(state: documentsState), - ) ?? + context: context, + builder: (context) => + BulkDeleteConfirmationDialog(state: documentsState)) ?? false; if (shouldDelete) { try { diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 219d5d6..453ff2e 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -26,7 +26,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { int _currentIndex = 0; - + final DocumentScannerCubit _scannerCubit = DocumentScannerCubit(); @override void initState() { super.initState(); @@ -35,52 +35,47 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return BlocConsumer( + return BlocListener( //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()), + ), + BlocProvider( + create: (context) => SavedViewCubit( + RepositoryProvider.of(context), + ), + ), + ], + child: const DocumentsPage(), ), - drawer: const InfoDrawer(), - body: [ - MultiBlocProvider( - providers: [ - BlocProvider.value( - value: DocumentsCubit(getIt()), - ), - BlocProvider( - create: (context) => SavedViewCubit( - RepositoryProvider.of(context), - ), - ), - ], - child: const DocumentsPage(), - ), - BlocProvider.value( - value: DocumentScannerCubit(), - child: const ScannerPage(), - ), - BlocProvider.value( - value: DocumentsCubit(getIt()), - child: const LabelsPage(), - ), - ][_currentIndex], - ); - }, + BlocProvider.value( + value: _scannerCubit, + child: const ScannerPage(), + ), + BlocProvider.value( + value: DocumentsCubit(getIt()), + child: const LabelsPage(), + ), + ][_currentIndex], + ), ); } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 4dfaef2..2d71d09 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -216,7 +216,7 @@ class _InboxPageState extends State { showSnackBar( context, S.of(context).inboxPageDocumentRemovedMessageText, - action: SnackBarAction( + action: SnackBarActionConfig( label: S.of(context).inboxPageUndoRemoveText, onPressed: () => _onUndoMarkAsSeen(doc, removedTags), ), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index e0dbe07..0c4e4fb 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -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 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( + 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>( + context), ), - Tab( - icon: Icon( - Icons.description_outlined, - color: Theme.of(context).colorScheme.onPrimaryContainer, + child: LabelTabView( + 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>( + context), ), - Tab( - icon: Icon( - Icons.folder_open, - color: Theme.of(context).colorScheme.onPrimaryContainer, + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: DocumentTypeQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, ), - ) - ], - ), - ), - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - BlocProvider( - create: (context) => LabelCubit( - RepositoryProvider.of>(context), - ), - child: LabelTabView( - 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>(context), - ), - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - documentType: DocumentTypeQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: LabelTabView( + 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( - RepositoryProvider.of>(context), - ), - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>( + context), + ), + child: LabelTabView( + 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( - RepositoryProvider.of>(context), - ), - child: LabelTabView( - 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, - ), - ), - ], - ), + ); + }, ), ); } diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 5a8feeb..bf2ea54 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -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 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( + 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>( builder: (context, state) { @@ -86,7 +93,7 @@ class _ScannerPageState extends State BlocBuilder>( 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 BlocProvider.of(context).state, ); final bytes = await doc.save(); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => LabelRepositoriesProvider( - child: BlocProvider( - create: (context) => DocumentUploadCubit( - localVault: getIt(), - documentApi: getIt(), - correspondentRepository: - RepositoryProvider.of>( - context, + final uploaded = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => LabelRepositoriesProvider( + child: BlocProvider( + create: (context) => DocumentUploadCubit( + localVault: getIt(), + documentApi: getIt(), + correspondentRepository: + RepositoryProvider.of>( + context, + ), + documentTypeRepository: + RepositoryProvider.of>( + context, + ), + tagRepository: RepositoryProvider.of>( + context, + ), + ), + child: DocumentUploadPreparationPage( + fileBytes: bytes, + ), ), - documentTypeRepository: - RepositoryProvider.of>( - context, - ), - tagRepository: RepositoryProvider.of>( - context, - ), - ), - child: DocumentUploadPreparationPage( - fileBytes: bytes, ), ), - ), - ), - ); + ) ?? + false; + if (uploaded) { + BlocProvider.of(context).reset(); + } } - Widget _buildBody() { + Widget _buildBody(bool isConnected) { return BlocBuilder>( builder: (context, scans) { if (scans.isNotEmpty) { @@ -181,7 +192,7 @@ class _ScannerPageState extends State child: Text(S .of(context) .documentScannerPageUploadFromThisDeviceButtonLabel), - onPressed: _onUploadFromFilesystem, + onPressed: isConnected ? _onUploadFromFilesystem : null, ), ], ), @@ -195,7 +206,7 @@ class _ScannerPageState extends State return GridView.builder( itemCount: scans.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, + crossAxisCount: 3, childAspectRatio: 1 / sqrt(2), crossAxisSpacing: 10, mainAxisSpacing: 10, diff --git a/lib/features/scan/view/widgets/grid_image_item_widget.dart b/lib/features/scan/view/widgets/grid_image_item_widget.dart index ec69378..9d1e317 100644 --- a/lib/features/scan/view/widgets/grid_image_item_widget.dart +++ b/lib/features/scan/view/widgets/grid_image_item_widget.dart @@ -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 { - bool isProcessing = false; @override Widget build(BuildContext context) { return GestureDetector( @@ -37,70 +37,86 @@ class _GridImageItemWidgetState extends State { ); } - 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}", - ); - } } diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 25acae5..dbfe0cd 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -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", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 59f6c6d..5bc6ef7 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -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", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1a6b1f5..bb4a13d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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", diff --git a/lib/main.dart b/lib/main.dart index c273089..94e37c7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { 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 { 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 { 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 { } 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 { final success = await Navigator.push( context, MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), + builder: (context) => BlocProvider( + create: (BuildContext context) => DocumentUploadCubit( + localVault: getIt(), + documentApi: getIt(), + tagRepository: RepositoryProvider.of>( + context, + ), + correspondentRepository: + RepositoryProvider.of>( + context, + ), + documentTypeRepository: + RepositoryProvider.of>( + context, + ), + ), child: DocumentUploadPreparationPage( fileBytes: bytes, filename: filename, @@ -244,6 +259,7 @@ class _AuthenticationWrapperState extends State { @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(context).logout(), - child: Text("Log out"), + child: const Text("Log out"), ), ElevatedButton( onPressed: () => BlocProvider.of(context) .restoreSessionState(), - child: Text("Authenticate"), + child: const Text("Authenticate"), ), ], ), diff --git a/lib/util.dart b/lib/util.dart index 6acfeac..f46b36c 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -16,11 +16,21 @@ final dateFormat = DateFormat("yyyy-MM-dd"); final GlobalKey rootScaffoldKey = GlobalKey(); 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.",