From 748cd21713c7dea0b6f545db445a996c4e91f6fa Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 1 Feb 2023 00:27:56 +0100 Subject: [PATCH] Added search bar on all pages --- lib/core/widgets/app_options_popup_menu.dart | 412 +++++++++--------- .../material/search/m3_search_bar.dart | 5 +- lib/core/widgets/paperless_logo.dart | 20 +- lib/features/app_drawer/view/app_drawer.dart | 117 +++++ .../bloc/document_details_cubit.dart | 7 +- .../view/pages/document_details_page.dart | 1 - .../cubit/document_search_cubit.dart | 2 +- .../cubit/document_search_state.dart | 7 +- .../cubit/document_search_state.g.dart | 0 .../view/document_search_page.dart | 21 +- .../view/documents_search_app_bar.dart | 0 .../documents/bloc/documents_cubit.dart | 5 +- .../documents/view/pages/documents_page.dart | 46 +- lib/features/home/view/home_page.dart | 40 +- lib/features/inbox/view/pages/inbox_page.dart | 198 ++++----- .../inbox/view/widgets/inbox_item.dart | 25 +- .../tags/view/widgets/tags_form_field.dart | 12 + .../labels/view/pages/labels_page.dart | 400 +++++++++++------ .../labels/view/widgets/label_tab_view.dart | 73 ++-- .../view/pages/linked_documents_page.dart | 14 + .../saved_view/view/saved_view_list.dart | 3 +- .../saved_view/view/saved_view_page.dart | 25 +- lib/features/scan/view/scanner_page.dart | 96 +++- .../search_app_bar/view/search_app_bar.dart | 17 +- .../view/dialogs/account_settings_dialog.dart | 103 +++++ lib/features/settings/view/settings_page.dart | 37 +- lib/l10n/intl_cs.arb | 3 +- lib/l10n/intl_de.arb | 3 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_tr.arb | 3 +- lib/main.dart | 5 + lib/routes/document_details_route.dart | 46 ++ .../lib/src/models/document_model.dart | 8 +- pubspec.lock | 16 + pubspec.yaml | 2 + 35 files changed, 1122 insertions(+), 653 deletions(-) rename lib/features/{search => document_search}/cubit/document_search_cubit.dart (94%) rename lib/features/{search => document_search}/cubit/document_search_state.dart (95%) rename lib/features/{search => document_search}/cubit/document_search_state.g.dart (100%) rename lib/features/{search => document_search}/view/document_search_page.dart (85%) rename lib/features/{search => document_search}/view/documents_search_app_bar.dart (100%) create mode 100644 lib/features/settings/view/dialogs/account_settings_dialog.dart create mode 100644 lib/routes/document_details_route.dart diff --git a/lib/core/widgets/app_options_popup_menu.dart b/lib/core/widgets/app_options_popup_menu.dart index ba97902..f02feb5 100644 --- a/lib/core/widgets/app_options_popup_menu.dart +++ b/lib/core/widgets/app_options_popup_menu.dart @@ -1,217 +1,217 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/constants.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; -import 'package:paperless_mobile/features/settings/view/settings_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:url_launcher/link.dart'; -import 'package:url_launcher/url_launcher_string.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:paperless_mobile/constants.dart'; +// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +// import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; +// import 'package:paperless_mobile/features/settings/model/view_type.dart'; +// import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +// import 'package:paperless_mobile/generated/l10n.dart'; +// import 'package:url_launcher/link.dart'; +// import 'package:url_launcher/url_launcher_string.dart'; -/// Declares selectable actions in menu. -enum AppPopupMenuEntries { - // Documents preview - documentsSelectListView, - documentsSelectGridView, - // Generic actions - openAboutThisAppDialog, - reportBug, - openSettings, - // Adds a divider - divider; -} +// /// Declares selectable actions in menu. +// enum AppPopupMenuEntries { +// // Documents preview +// documentsSelectListView, +// documentsSelectGridView, +// // Generic actions +// openAboutThisAppDialog, +// reportBug, +// openSettings, +// // Adds a divider +// divider; +// } -class AppOptionsPopupMenu extends StatelessWidget { - final List displayedActions; - const AppOptionsPopupMenu({ - super.key, - required this.displayedActions, - }); +// class AppOptionsPopupMenu extends StatelessWidget { +// final List displayedActions; +// const AppOptionsPopupMenu({ +// super.key, +// required this.displayedActions, +// }); - @override - Widget build(BuildContext context) { - return PopupMenuButton( - position: PopupMenuPosition.under, - icon: const Icon(Icons.more_vert), - onSelected: (action) { - switch (action) { - case AppPopupMenuEntries.documentsSelectListView: - context.read().setViewType(ViewType.list); - break; - case AppPopupMenuEntries.documentsSelectGridView: - context.read().setViewType(ViewType.grid); - break; - case AppPopupMenuEntries.openAboutThisAppDialog: - _showAboutDialog(context); - break; - case AppPopupMenuEntries.openSettings: - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: context.read(), - child: const SettingsPage(), - ), - ), - ); - break; - case AppPopupMenuEntries.reportBug: - launchUrlString( - 'https://github.com/astubenbord/paperless-mobile/issues/new', - ); - break; - default: - break; - } - }, - itemBuilder: _buildEntries, - ); - } +// @override +// Widget build(BuildContext context) { +// return PopupMenuButton( +// position: PopupMenuPosition.under, +// icon: const Icon(Icons.more_vert), +// onSelected: (action) { +// switch (action) { +// case AppPopupMenuEntries.documentsSelectListView: +// context.read().setViewType(ViewType.list); +// break; +// case AppPopupMenuEntries.documentsSelectGridView: +// context.read().setViewType(ViewType.grid); +// break; +// case AppPopupMenuEntries.openAboutThisAppDialog: +// _showAboutDialog(context); +// break; +// case AppPopupMenuEntries.openSettings: +// Navigator.of(context).push( +// MaterialPageRoute( +// builder: (context) => BlocProvider.value( +// value: context.read(), +// child: const SettingsPage(), +// ), +// ), +// ); +// break; +// case AppPopupMenuEntries.reportBug: +// launchUrlString( +// 'https://github.com/astubenbord/paperless-mobile/issues/new', +// ); +// break; +// default: +// break; +// } +// }, +// itemBuilder: _buildEntries, +// ); +// } - PopupMenuItem _buildReportBugTile(BuildContext context) { - return PopupMenuItem( - value: AppPopupMenuEntries.reportBug, - padding: EdgeInsets.zero, - child: ListTile( - leading: const Icon(Icons.bug_report), - title: Text(S.of(context).appDrawerReportBugLabel), - ), - ); - } +// PopupMenuItem _buildReportBugTile(BuildContext context) { +// return PopupMenuItem( +// value: AppPopupMenuEntries.reportBug, +// padding: EdgeInsets.zero, +// child: ListTile( +// leading: const Icon(Icons.bug_report), +// title: Text(S.of(context).appDrawerReportBugLabel), +// ), +// ); +// } - PopupMenuItem _buildSettingsTile(BuildContext context) { - return PopupMenuItem( - padding: EdgeInsets.zero, - value: AppPopupMenuEntries.openSettings, - child: ListTile( - leading: const Icon(Icons.settings_outlined), - title: Text(S.of(context).appDrawerSettingsLabel), - ), - ); - } +// PopupMenuItem _buildSettingsTile(BuildContext context) { +// return PopupMenuItem( +// padding: EdgeInsets.zero, +// value: AppPopupMenuEntries.openSettings, +// child: ListTile( +// leading: const Icon(Icons.settings_outlined), +// title: Text(S.of(context).appDrawerSettingsLabel), +// ), +// ); +// } - PopupMenuItem _buildAboutTile(BuildContext context) { - return PopupMenuItem( - padding: EdgeInsets.zero, - value: AppPopupMenuEntries.openAboutThisAppDialog, - child: ListTile( - leading: const Icon(Icons.info_outline), - title: Text(S.of(context).appDrawerAboutLabel), - ), - ); - } +// PopupMenuItem _buildAboutTile(BuildContext context) { +// return PopupMenuItem( +// padding: EdgeInsets.zero, +// value: AppPopupMenuEntries.openAboutThisAppDialog, +// child: ListTile( +// leading: const Icon(Icons.info_outline), +// title: Text(S.of(context).appDrawerAboutLabel), +// ), +// ); +// } - PopupMenuItem _buildListViewTile() { - return PopupMenuItem( - padding: EdgeInsets.zero, - child: BlocBuilder( - builder: (context, state) { - return ListTile( - leading: const Icon(Icons.list), - title: const Text("List"), - trailing: state.preferredViewType == ViewType.list - ? const Icon(Icons.check) - : null, - ); - }, - ), - value: AppPopupMenuEntries.documentsSelectListView, - ); - } +// PopupMenuItem _buildListViewTile() { +// return PopupMenuItem( +// padding: EdgeInsets.zero, +// child: BlocBuilder( +// builder: (context, state) { +// return ListTile( +// leading: const Icon(Icons.list), +// title: const Text("List"), +// trailing: state.preferredViewType == ViewType.list +// ? const Icon(Icons.check) +// : null, +// ); +// }, +// ), +// value: AppPopupMenuEntries.documentsSelectListView, +// ); +// } - PopupMenuItem _buildGridViewTile() { - return PopupMenuItem( - value: AppPopupMenuEntries.documentsSelectGridView, - padding: EdgeInsets.zero, - child: BlocBuilder( - builder: (context, state) { - return ListTile( - leading: const Icon(Icons.grid_view_rounded), - title: const Text("Grid"), - trailing: state.preferredViewType == ViewType.grid - ? const Icon(Icons.check) - : null, - ); - }, - ), - ); - } +// PopupMenuItem _buildGridViewTile() { +// return PopupMenuItem( +// value: AppPopupMenuEntries.documentsSelectGridView, +// padding: EdgeInsets.zero, +// child: BlocBuilder( +// builder: (context, state) { +// return ListTile( +// leading: const Icon(Icons.grid_view_rounded), +// title: const Text("Grid"), +// trailing: state.preferredViewType == ViewType.grid +// ? const Icon(Icons.check) +// : null, +// ); +// }, +// ), +// ); +// } - void _showAboutDialog(BuildContext context) { - showAboutDialog( - context: context, - applicationIcon: const ImageIcon( - AssetImage('assets/logos/paperless_logo_green.png'), - ), - applicationName: 'Paperless Mobile', - applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, - children: [ - Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), - Link( - uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), - builder: (context, followLink) => GestureDetector( - onTap: followLink, - child: Text( - 'https://github.com/astubenbord/paperless-mobile', - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), - ), - ), - ), - const SizedBox(height: 16), - Text( - 'Credits', - style: Theme.of(context).textTheme.titleMedium, - ), - _buildOnboardingImageCredits(), - ], - ); - } +// void _showAboutDialog(BuildContext context) { +// showAboutDialog( +// context: context, +// applicationIcon: const ImageIcon( +// AssetImage('assets/logos/paperless_logo_green.png'), +// ), +// applicationName: 'Paperless Mobile', +// applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, +// children: [ +// Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), +// Link( +// uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), +// builder: (context, followLink) => GestureDetector( +// onTap: followLink, +// child: Text( +// 'https://github.com/astubenbord/paperless-mobile', +// style: TextStyle(color: Theme.of(context).colorScheme.tertiary), +// ), +// ), +// ), +// const SizedBox(height: 16), +// Text( +// 'Credits', +// style: Theme.of(context).textTheme.titleMedium, +// ), +// _buildOnboardingImageCredits(), +// ], +// ); +// } - Widget _buildOnboardingImageCredits() { - return Link( - uri: Uri.parse( - 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), - builder: (context, followLink) => Wrap( - children: [ - const Text('Onboarding images by '), - GestureDetector( - onTap: followLink, - child: Text( - 'pch.vector', - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), - ), - ), - const Text(' on Freepik.') - ], - ), - ); - } +// Widget _buildOnboardingImageCredits() { +// return Link( +// uri: Uri.parse( +// 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), +// builder: (context, followLink) => Wrap( +// children: [ +// const Text('Onboarding images by '), +// GestureDetector( +// onTap: followLink, +// child: Text( +// 'pch.vector', +// style: TextStyle(color: Theme.of(context).colorScheme.tertiary), +// ), +// ), +// const Text(' on Freepik.') +// ], +// ), +// ); +// } - List> _buildEntries( - BuildContext context) { - List> items = []; - for (final entry in displayedActions) { - switch (entry) { - case AppPopupMenuEntries.documentsSelectListView: - items.add(_buildListViewTile()); - break; - case AppPopupMenuEntries.documentsSelectGridView: - items.add(_buildGridViewTile()); - break; - case AppPopupMenuEntries.openAboutThisAppDialog: - items.add(_buildAboutTile(context)); - break; - case AppPopupMenuEntries.reportBug: - items.add(_buildReportBugTile(context)); - break; - case AppPopupMenuEntries.openSettings: - items.add(_buildSettingsTile(context)); - break; - case AppPopupMenuEntries.divider: - items.add(const PopupMenuDivider()); - break; - } - } - return items; - } -} +// List> _buildEntries( +// BuildContext context) { +// List> items = []; +// for (final entry in displayedActions) { +// switch (entry) { +// case AppPopupMenuEntries.documentsSelectListView: +// items.add(_buildListViewTile()); +// break; +// case AppPopupMenuEntries.documentsSelectGridView: +// items.add(_buildGridViewTile()); +// break; +// case AppPopupMenuEntries.openAboutThisAppDialog: +// items.add(_buildAboutTile(context)); +// break; +// case AppPopupMenuEntries.reportBug: +// items.add(_buildReportBugTile(context)); +// break; +// case AppPopupMenuEntries.openSettings: +// items.add(_buildSettingsTile(context)); +// break; +// case AppPopupMenuEntries.divider: +// items.add(const PopupMenuDivider()); +// break; +// } +// } +// return items; +// } +// } diff --git a/lib/core/widgets/material/search/m3_search_bar.dart b/lib/core/widgets/material/search/m3_search_bar.dart index eb75144..dafcea1 100644 --- a/lib/core/widgets/material/search/m3_search_bar.dart +++ b/lib/core/widgets/material/search/m3_search_bar.dart @@ -39,7 +39,7 @@ class SearchBar extends StatelessWidget { surfaceTintColor: colorScheme.surfaceTint, borderRadius: BorderRadius.circular(effectiveHeight / 2), child: InkWell( - onTap: () {}, + onTap: onTap, borderRadius: BorderRadius.circular(effectiveHeight / 2), highlightColor: Colors.transparent, splashFactory: InkRipple.splashFactory, @@ -51,7 +51,9 @@ class SearchBar extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(right: 8), child: TextField( + onTap: onTap, readOnly: true, + enabled: false, cursorColor: colorScheme.primary, style: textTheme.bodyLarge, textAlignVertical: TextAlignVertical.center, @@ -64,7 +66,6 @@ class SearchBar extends StatelessWidget { color: colorScheme.onSurfaceVariant, ), ), - onTap: onTap, ), ), ), diff --git a/lib/core/widgets/paperless_logo.dart b/lib/core/widgets/paperless_logo.dart index 1f5b08b..38e3b0e 100644 --- a/lib/core/widgets/paperless_logo.dart +++ b/lib/core/widgets/paperless_logo.dart @@ -2,18 +2,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class PaperlessLogo extends StatelessWidget { + static const _paperlessGreen = Color(0xFF18541F); final double? height; final double? width; - final String _path; + final Color _color; - const PaperlessLogo.white({super.key, this.height, this.width}) - : _path = "assets/logos/paperless_logo_white.svg"; + const PaperlessLogo.white({ + super.key, + this.height, + this.width, + }) : _color = Colors.white; const PaperlessLogo.green({super.key, this.height, this.width}) - : _path = "assets/logos/paperless_logo_green.svg"; + : _color = _paperlessGreen; const PaperlessLogo.black({super.key, this.height, this.width}) - : _path = "assets/logos/paperless_logo_black.svg"; + : _color = Colors.black; + + const PaperlessLogo.colored(Color color, {super.key, this.height, this.width}) + : _color = color; @override Widget build(BuildContext context) { @@ -24,7 +31,8 @@ class PaperlessLogo extends StatelessWidget { ), padding: const EdgeInsets.only(right: 8), child: SvgPicture.asset( - _path, + "assets/logos/paperless_logo_white.svg", + color: _color, ), ); } diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart index e69de29..0aae097 100644 --- a/lib/features/app_drawer/view/app_drawer.dart +++ b/lib/features/app_drawer/view/app_drawer.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/constants.dart'; +import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:url_launcher/link.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: true, + child: Drawer( + child: Column( + children: [ + Row( + children: [ + const PaperlessLogo.green(), + Text( + "Paperless Mobile", + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ).padded(), + const Divider(), + ListTile( + dense: true, + title: Text(S.of(context).appDrawerAboutLabel), + leading: const Icon(Icons.info_outline), + onTap: () => _showAboutDialog(context), + ), + ListTile( + dense: true, + leading: const Icon(Icons.bug_report_outlined), + title: Text(S.of(context).appDrawerReportBugLabel), + onTap: () { + launchUrlString( + 'https://github.com/astubenbord/paperless-mobile/issues/new'); + }, + ), + ListTile( + dense: true, + leading: const Icon(Icons.settings_outlined), + title: Text( + S.of(context).appDrawerSettingsLabel, + ), + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: const SettingsPage(), + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _showAboutDialog(BuildContext context) { + showAboutDialog( + context: context, + applicationIcon: const ImageIcon( + AssetImage('assets/logos/paperless_logo_green.png'), + ), + applicationName: 'Paperless Mobile', + applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, + children: [ + Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), + Link( + uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + 'https://github.com/astubenbord/paperless-mobile', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Credits', + style: Theme.of(context).textTheme.titleMedium, + ), + _buildOnboardingImageCredits(), + ], + ); + } + + Widget _buildOnboardingImageCredits() { + return Link( + uri: Uri.parse( + 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), + builder: (context, followLink) => Wrap( + children: [ + const Text('Onboarding images by '), + GestureDetector( + onTap: followLink, + child: Text( + 'pch.vector', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + const Text(' on Freepik.') + ], + ), + ); + } +} diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 0752697..68772c8 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -1,15 +1,10 @@ -import 'dart:developer'; import 'dart:io'; -import 'dart:typed_data'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:open_filex/open_filex.dart'; part 'document_details_state.dart'; 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 3a03f58..203ac30 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -482,7 +482,6 @@ class _DocumentDetailsPageState extends State { child: _DetailsItem( label: S.of(context).documentStoragePathPropertyLabel, content: StoragePathWidget( - isClickable: widget.isLabelClickable, pathId: document.storagePath, ), ).paddedSymmetrically(vertical: 16), diff --git a/lib/features/search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart similarity index 94% rename from lib/features/search/cubit/document_search_cubit.dart rename to lib/features/document_search/cubit/document_search_cubit.dart index 390c68b..2707926 100644 --- a/lib/features/search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; class DocumentSearchCubit extends HydratedCubit with PagedDocumentsMixin { diff --git a/lib/features/search/cubit/document_search_state.dart b/lib/features/document_search/cubit/document_search_state.dart similarity index 95% rename from lib/features/search/cubit/document_search_state.dart rename to lib/features/document_search/cubit/document_search_state.dart index 6667d7f..231405d 100644 --- a/lib/features/search/cubit/document_search_state.dart +++ b/lib/features/document_search/cubit/document_search_state.dart @@ -27,11 +27,8 @@ class DocumentSearchState extends PagedDocumentsState { }); @override - List get props => [ - hasLoaded, - isLoading, - filter, - value, + List get props => [ + ...super.props, searchHistory, suggestions, view, diff --git a/lib/features/search/cubit/document_search_state.g.dart b/lib/features/document_search/cubit/document_search_state.g.dart similarity index 100% rename from lib/features/search/cubit/document_search_state.g.dart rename to lib/features/document_search/cubit/document_search_state.g.dart diff --git a/lib/features/search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart similarity index 85% rename from lib/features/search/view/document_search_page.dart rename to lib/features/document_search/view/document_search_page.dart index a7255a2..9608dfb 100644 --- a/lib/features/search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -1,11 +1,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; Future showDocumentSearchPage(BuildContext context) { return Navigator.of(context).push( @@ -151,12 +153,27 @@ class _DocumentSearchPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, + onTap: (document) async { + final updatedDocument = await Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ) as DocumentModel?; + if (updatedDocument != document) { + context.read().reload(); + } + }, ) ], ); } void _selectSuggestion(String suggestion) { + _queryController.text = suggestion; context.read().search(suggestion); + FocusScope.of(context).unfocus(); } } diff --git a/lib/features/search/view/documents_search_app_bar.dart b/lib/features/document_search/view/documents_search_app_bar.dart similarity index 100% rename from lib/features/search/view/documents_search_app_bar.dart rename to lib/features/document_search/view/documents_search_app_bar.dart diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index eefd2c6..51c6b02 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -12,10 +12,7 @@ class DocumentsCubit extends HydratedCubit @override final PaperlessDocumentsApi api; - final SavedViewRepository _savedViewRepository; - - DocumentsCubit(this.api, this._savedViewRepository) - : super(const DocumentsState()); + DocumentsCubit(this.api) : super(const DocumentsState()); Future bulkRemove(List documents) async { log("[DocumentsCubit] bulkRemove"); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 8f8c7ef..1f1c3d7 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -1,33 +1,28 @@ -import 'dart:developer'; - import 'package:badges/badges.dart' as b; import 'package:collection/collection.dart'; 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/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; -import 'package:paperless_mobile/features/search/view/document_search_page.dart'; import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -116,6 +111,7 @@ class _DocumentsPageState extends State }, builder: (context, connectivityState) { return Scaffold( + drawer: const AppDrawer(), floatingActionButton: BlocBuilder( builder: (context, state) { final appliedFiltersCount = state.filter.appliedFiltersCount; @@ -165,6 +161,7 @@ class _DocumentsPageState extends State context, ), sliver: SearchAppBar( + hintText: "Search documents", //TODO: INTL onOpenSearch: showDocumentSearchPage, bottom: TabBar( controller: _tabController, @@ -177,9 +174,12 @@ class _DocumentsPageState extends State ), ), ], - body: NotificationListener( + body: NotificationListener( onNotification: (notification) { final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } final desiredTab = (metrics.pixels / metrics.maxScrollExtent).round(); if (metrics.axis == Axis.horizontal && @@ -414,29 +414,21 @@ class _DocumentsPageState extends State } Future _openDetails(DocumentModel document) async { - final updatedModel = await Navigator.of(context).push( - _buildDetailsPageRoute(document), - ); + final updatedModel = await Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + ), + ) as DocumentModel?; + // final updatedModel = await Navigator.of(context).push( + // _buildDetailsPageRoute(document), + // ); if (updatedModel != document) { context.read().reload(); } } - MaterialPageRoute _buildDetailsPageRoute( - DocumentModel document) { - return MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage(), - ), - ), - ); - } - void _addTagToFilter(int tagId) { try { final tagsQuery = diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b033504..b0288b5 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -48,11 +48,18 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { int _currentIndex = 0; final DocumentScannerCubit _scannerCubit = DocumentScannerCubit(); + late final InboxCubit _inboxCubit; @override void initState() { super.initState(); _initializeData(context); + _inboxCubit = InboxCubit( + context.read(), + context.read(), + context.read(), + context.read(), + ); context.read().reload(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _listenForReceivedFiles(); @@ -147,6 +154,12 @@ class _HomePageState extends State { } } + @override + void dispose() { + _inboxCubit.close(); + super.dispose(); + } + @override Widget build(BuildContext context) { final destinations = [ @@ -182,28 +195,15 @@ class _HomePageState extends State { ), label: S.of(context).bottomNavInboxPageLabel, ), - // RouteDescription( - // icon: const Icon(Icons.settings_outlined), - // selectedIcon: Icon( - // Icons.settings, - // color: Theme.of(context).colorScheme.primary, - // ), - // label: S.of(context).appDrawerSettingsLabel, - // ), ]; final routes = [ MultiBlocProvider( providers: [ BlocProvider( - create: (context) => DocumentsCubit( - context.read(), - context.read(), - ), + create: (context) => DocumentsCubit(context.read()), ), BlocProvider( - create: (context) => SavedViewCubit( - context.read(), - ), + create: (context) => SavedViewCubit(context.read()), ), ], child: const DocumentsPage(), @@ -213,13 +213,8 @@ class _HomePageState extends State { child: const ScannerPage(), ), const LabelsPage(), - BlocProvider( - create: (context) => InboxCubit( - context.read(), - context.read(), - context.read(), - context.read(), - ), + BlocProvider.value( + value: _inboxCubit, child: const InboxPage(), ), // const SettingsPage(), @@ -249,7 +244,6 @@ class _HomePageState extends State { builder: (context, sizingInformation) { if (!sizingInformation.isMobile) { return Scaffold( - // drawer: const AppDrawer(), body: Row( children: [ NavigationRail( diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index d70f5f2..b607538 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; @@ -11,6 +13,7 @@ import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; @@ -54,39 +57,8 @@ class _InboxPageState extends State { @override Widget build(BuildContext context) { - const _progressBarHeight = 4.0; return Scaffold( - appBar: PreferredSize( - preferredSize: - const Size.fromHeight(kToolbarHeight + _progressBarHeight), - child: BlocBuilder( - builder: (context, state) { - return AppBar( - title: Text(S.of(context).bottomNavInboxPageLabel), - actions: [ - if (state.hasLoaded) - Align( - alignment: Alignment.centerRight, - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: ColoredBox( - color: Theme.of(context).colorScheme.secondaryContainer, - child: Text( - (state.value.isEmpty - ? '0 ' - : '${state.value.first.count} ') + - S.of(context).inboxPageUnseenText, - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.bodySmall, - ).paddedSymmetrically(horizontal: 4.0), - ), - ), - ).paddedSymmetrically(horizontal: 8) - ], - ); - }, - ), - ), + drawer: const AppDrawer(), floatingActionButton: BlocBuilder( builder: (context, state) { if (!state.hasLoaded || state.documents.isEmpty) { @@ -104,91 +76,95 @@ class _InboxPageState extends State { ); }, ), - body: BlocBuilder( - builder: (context, state) { - if (!state.hasLoaded) { - return const DocumentsListLoadingWidget(); - } + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SearchAppBar( + hintText: "Search documents", + onOpenSearch: showDocumentSearchPage, + ), + ], + body: BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded) { + return const CustomScrollView( + physics: NeverScrollableScrollPhysics(), + slivers: [DocumentsListLoadingWidget()], + ); + } - if (state.documents.isEmpty) { - return InboxEmptyWidget( - emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, - ); - } + if (state.documents.isEmpty) { + return InboxEmptyWidget( + emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, + ); + } - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - final List slivers = _groupByDate(state.documents) - .entries - .map( - (entry) => [ - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerLeft, - child: ClipRRect( - borderRadius: BorderRadius.circular(32.0), - child: Text( - entry.key, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ).padded(), - ), - ).paddedOnly(top: 8.0), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: entry.value.length, - (context, index) { - if (index < entry.value.length - 1) { - return Column( - children: [ - _buildListItem( - entry.value[index], - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ], + // Build a list of slivers alternating between SliverToBoxAdapter + // (group header) and a SliverList (inbox items). + final List slivers = _groupByDate(state.documents) + .entries + .map( + (entry) => [ + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Text( + entry.key, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ).padded(), + ), + ).paddedOnly(top: 8.0), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: entry.value.length, + (context, index) { + if (index < entry.value.length - 1) { + return Column( + children: [ + _buildListItem( + entry.value[index], + ), + const Divider( + indent: 16, + endIndent: 16, + ), + ], + ); + } + return _buildListItem( + entry.value[index], ); - } - return _buildListItem( - entry.value[index], - ); - }, + }, + ), + ), + ], + ) + .flattened + .toList() + ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); + + return RefreshIndicator( + onRefresh: () => context.read().initializeInbox(), + child: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverToBoxAdapter( + child: HintCard( + show: !state.isHintAcknowledged, + hintText: S.of(context).inboxPageUsageHintText, + onHintAcknowledged: () => + context.read().acknowledgeHint(), ), ), + ...slivers, ], - ) - .flattened - .toList() - ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); - - return RefreshIndicator( - onRefresh: () => context.read().initializeInbox(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: CustomScrollView( - controller: _scrollController, - slivers: [ - SliverToBoxAdapter( - child: HintCard( - show: !state.isHintAcknowledged, - hintText: S.of(context).inboxPageUsageHintText, - onHintAcknowledged: () => - context.read().acknowledgeHint(), - ), - ), - ...slivers, - ], - ), - ), - ], - ), - ); - }, + ), + ); + }, + ), ), ); } diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 2367594..0f5709e 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class InboxItem extends StatefulWidget { static const _a4AspectRatio = 1 / 1.4142; @@ -40,24 +41,16 @@ class _InboxItemState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - final returnedDocument = await Navigator.push( + final updatedDocument = await Navigator.pushNamed( context, - MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - widget.document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage( - isLabelClickable: false, - ), - ), - ), + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: widget.document, + isLabelClickable: false, ), - ); - if (returnedDocument != null) { - widget.onDocumentUpdated(returnedDocument); + ) as DocumentModel?; + if (updatedDocument != null) { + widget.onDocumentUpdated(updatedDocument); } }, child: SizedBox( diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index c1e2a4f..3ea2ae4 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -266,6 +266,10 @@ class _TagFormFieldState extends State { Widget _buildNotAssignedTag(FormFieldState field) { return ColoredChipWrapper( child: InputChip( + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, label: Text( S.of(context).labelNotAssignedText, ), @@ -288,6 +292,10 @@ class _TagFormFieldState extends State { } return ColoredChipWrapper( child: InputChip( + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, label: Text( tag.name, style: TextStyle( @@ -312,6 +320,10 @@ class _TagFormFieldState extends State { Widget _buildAnyAssignedTag(FormFieldState field) { return ColoredChipWrapper( child: InputChip( + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, label: Text(S.of(context).labelAnyAssignedText), backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index f84bb3a..d64521c 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -8,6 +10,9 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.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'; @@ -16,12 +21,13 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_corresponden import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart'; -import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class LabelsPage extends StatefulWidget { @@ -51,154 +57,264 @@ class _LabelsPageState extends State child: BlocBuilder( builder: (context, connectedState) { return Scaffold( - 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), - ) + drawer: const AppDrawer(), + floatingActionButton: FloatingActionButton( + onPressed: [ + _openAddCorrespondentPage, + _openAddDocumentTypePage, + _openAddTagPage, + _openAddStoragePathPage, + ][_currentIndex], + child: Icon(Icons.add), + ), + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + // This widget takes the overlapping behavior of the SliverAppBar, + // and redirects it to the SliverOverlapInjector below. If it is + // missing, then it is possible for the nested "inner" scroll view + // below to end up under the SliverAppBar even when the inner + // scroll view thinks it has not been scrolled. + // This is not necessary if the "headerSliverBuilder" only builds + // widgets that do not overlap the next sliver. + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + sliver: SearchAppBar( + hintText: "Search documents", //TODO: INTL + onOpenSearch: showDocumentSearchPage, + bottom: TabBar( + 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, + ), + ), + ], + ), + ), + ), ], - 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: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } + final desiredTab = + ((metrics.pixels / metrics.maxScrollExtent) * + (_tabController.length - 1)) + .round(); + + if (metrics.axis == Axis.horizontal && + _currentIndex != desiredTab) { + setState(() => _currentIndex = desiredTab); + } + return true; + }, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context + .read>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), ), ), ], + child: RefreshIndicator( + edgeOffset: kToolbarHeight, + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () => [ + context.read>(), + context.read>(), + context.read>(), + context.read>(), + ][_currentIndex] + .reload(), + child: TabBarView( + controller: _tabController, + children: [ + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + correspondent: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageCorrespondentEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageCorrespondentEmptyStateDescriptionText, + onAddNew: _openAddCorrespondentPage, + ), + ], + ); + }, + ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + DocumentTypeBlocProvider( + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageDocumentTypeEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageDocumentTypeEmptyStateDescriptionText, + onAddNew: _openAddDocumentTypePage, + ), + ), + ], + ); + }, + ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + TagBlocProvider( + 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, + ), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageTagsEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageTagsEmptyStateDescriptionText, + onAddNew: _openAddTagPage, + ), + ), + ], + ); + }, + ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + StoragePathBlocProvider( + child: LabelTabView( + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => + Text(path.path ?? ""), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageStoragePathEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageStoragePathEmptyStateDescriptionText, + onAddNew: _openAddStoragePathPage, + ), + ), + ], + ); + }, + ), + ], + ), + ), ), ), ), - body: TabBarView( - controller: _tabController, - children: [ - CorrespondentBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - correspondent: IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageCorrespondentEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageCorrespondentEmptyStateDescriptionText, - onAddNew: _openAddCorrespondentPage, - ), - ), - DocumentTypeBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - documentType: IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageDocumentTypeEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageDocumentTypeEmptyStateDescriptionText, - onAddNew: _openAddDocumentTypePage, - ), - ), - TagBlocProvider( - 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, - ), - emptyStateActionButtonLabel: - S.of(context).labelsPageTagsEmptyStateAddNewLabel, - emptyStateDescription: - S.of(context).labelsPageTagsEmptyStateDescriptionText, - onAddNew: _openAddTagPage, - ), - ), - StoragePathBlocProvider( - child: LabelTabView( - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: IdQueryParameter.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/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 76133a6..8aecad4 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -46,45 +46,46 @@ class LabelTabView extends StatelessWidget { } final labels = state.labels.values.toList()..sort(); if (labels.isEmpty) { - return Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - emptyStateDescription, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: onAddNew, - child: Text(emptyStateActionButtonLabel), - ), - ].padded(), + return SliverFillRemaining( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emptyStateDescription, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: onAddNew, + child: Text(emptyStateActionButtonLabel), + ), + ].padded(), + ), ), ); } - return RefreshIndicator( - onRefresh: context.read>().reload, - notificationPredicate: (notification) => - connectivityState.isConnected, - child: ListView( - children: labels - .map((l) => LabelItem( - name: l.name, - content: contentBuilder?.call(l) ?? - Text( - translateMatchingAlgorithmName( - context, l.matchingAlgorithm) + - ((l.match?.isNotEmpty ?? false) - ? ": ${l.match}" - : ""), - maxLines: 2, - ), - onOpenEditPage: onEdit, - filterBuilder: filterBuilder, - leading: leadingBuilder?.call(l), - label: l, - )) - .toList(), + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final l = labels.elementAt(index); + return LabelItem( + name: l.name, + content: contentBuilder?.call(l) ?? + Text( + translateMatchingAlgorithmName( + context, l.matchingAlgorithm) + + ((l.match?.isNotEmpty ?? false) + ? ": ${l.match}" + : ""), + maxLines: 2, + ), + onOpenEditPage: onEdit, + filterBuilder: filterBuilder, + leading: leadingBuilder?.call(l), + label: l, + ); + }, + childCount: labels.length, ), ); }, diff --git a/lib/features/linked_documents/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart index b0956c2..7724a01 100644 --- a/lib/features/linked_documents/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -11,6 +11,7 @@ import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); @@ -59,6 +60,19 @@ class _LinkedDocumentsPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, + onTap: (document) async { + final updatedDocument = await Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ) as DocumentModel?; + if (updatedDocument != document) { + context.read().reload(); + } + }, ); }, ); diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 04f09ef..c6648c2 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -29,7 +29,8 @@ class SavedViewList extends StatelessWidget { return ListTile( title: Text(view.name), subtitle: Text( - "${view.filterRules.length} filter(s) set"), //TODO: INTL w/ placeholder + "${view.filterRules.length} filter(s) set", + ), //TODO: INTL w/ placeholder onTap: () { Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/features/saved_view/view/saved_view_page.dart b/lib/features/saved_view/view/saved_view_page.dart index 84f7b20..cd3aa0f 100644 --- a/lib/features/saved_view/view/saved_view_page.dart +++ b/lib/features/saved_view/view/saved_view_page.dart @@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/view_actions.da import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class SavedViewPage extends StatefulWidget { final Future Function(SavedView savedView) onDelete; @@ -101,6 +102,12 @@ class _SavedViewPageState extends State { onTap: _onOpenDocumentDetails, viewType: _viewType, ), + if (state.hasLoaded && state.isLoading) + const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), + ), + ) ], ); }, @@ -111,20 +118,14 @@ class _SavedViewPageState extends State { } void _onOpenDocumentDetails(DocumentModel document) async { - final updatedDocument = await Navigator.push( + final updatedDocument = await Navigator.pushNamed( context, - MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage(), - ), - ), + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, ), - ); + ) as DocumentModel?; if (updatedDocument != document) { // Reload in case document was edited and might not fulfill filter criteria of saved view anymore context.read().reload(); diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index bfa8fc4..9f8740a 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -17,12 +17,14 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.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/documents/view/pages/document_view.dart'; -import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/widgets/scanned_image_item.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/file_helpers.dart'; @@ -47,20 +49,100 @@ class _ScannerPageState extends State return BlocBuilder( builder: (context, connectedState) { return Scaffold( + drawer: const AppDrawer(), 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(context, connectedState.isConnected), + // body: Padding( + // padding: const EdgeInsets.all(8.0), + // child: _buildBody(connectedState.isConnected), + // ), + body: BlocBuilder>( + builder: (context, state) { + return NestedScrollView( + floatHeaderSlivers: false, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SearchAppBar( + hintText: "Search documents", //TODO: INTL + onOpenSearch: showDocumentSearchPage, + bottom: PreferredSize( + child: _buildActions(connectedState.isConnected), + preferredSize: const Size.fromHeight(kTextTabBarHeight), + ), + ), + ], + body: CustomScrollView( + slivers: [ + if (state.isEmpty) + SliverFillRemaining( + child: _buildEmptyState(connectedState.isConnected), + ) + else + _buildImageGrid(state) + ], + ), + ); + }, ), ); }, ); } + Widget _buildActions(bool isConnected) { + return SizedBox( + height: kTextTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + BlocBuilder>( + builder: (context, state) { + return TextButton.icon( + label: Text("Preview"), //TODO: INTL + onPressed: state.isNotEmpty + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DocumentView( + documentBytes: _assembleFileBytes( + state, + forcePdf: true, + ).then((file) => file.bytes), + ), + ), + ) + : null, + icon: const Icon(Icons.visibility_outlined), + ); + }, + ), + BlocBuilder>( + builder: (context, state) { + return TextButton.icon( + label: Text("Clear all"), //TODO: INTL + onPressed: state.isEmpty ? null : () => _reset(context), + icon: const Icon(Icons.delete_sweep_outlined), + ); + }, + ), + BlocBuilder>( + builder: (context, state) { + return TextButton.icon( + label: Text("Upload"), //TODO: INTL + onPressed: state.isEmpty || !isConnected + ? null + : () => _onPrepareDocumentUpload(context), + icon: const Icon(Icons.upload_outlined), + ); + }, + ), + ], + ), + ); + } + AppBar _buildAppBar(BuildContext context, bool isConnected) { return AppBar( title: Text(S.of(context).documentScannerPageTitle), @@ -170,7 +252,7 @@ class _ScannerPageState extends State } } - Widget _buildBody(bool isConnected) { + Widget _buildEmptyState(bool isConnected) { return BlocBuilder>( builder: (context, scans) { if (scans.isNotEmpty) { @@ -207,7 +289,7 @@ class _ScannerPageState extends State } Widget _buildImageGrid(List scans) { - return GridView.builder( + return SliverGrid.builder( itemCount: scans.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, diff --git a/lib/features/search_app_bar/view/search_app_bar.dart b/lib/features/search_app_bar/view/search_app_bar.dart index 08ddb62..ee70481 100644 --- a/lib/features/search_app_bar/view/search_app_bar.dart +++ b/lib/features/search_app_bar/view/search_app_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; typedef OpenSearchCallback = void Function(BuildContext context); @@ -8,11 +9,13 @@ class SearchAppBar extends StatefulWidget with PreferredSizeWidget { final PreferredSizeWidget? bottom; final OpenSearchCallback onOpenSearch; final Color? backgroundColor; + final String hintText; const SearchAppBar({ super.key, required this.onOpenSearch, this.bottom, this.backgroundColor, + required this.hintText, }); @override @@ -26,25 +29,29 @@ class _SearchAppBarState extends State { @override Widget build(BuildContext context) { return SliverAppBar( + automaticallyImplyLeading: false, floating: true, pinned: true, snap: true, backgroundColor: widget.backgroundColor, title: SearchBar( height: kToolbarHeight - 8, - supportingText: "Search documents", + supportingText: widget.hintText, onTap: () => widget.onOpenSearch(context), leadingIcon: IconButton( icon: const Icon(Icons.menu), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, + onPressed: Scaffold.of(context).openDrawer, ), trailingIcon: IconButton( icon: const CircleAvatar( child: Text("A"), ), - onPressed: () {}, + onPressed: () { + showDialog( + context: context, + builder: (context) => AccountSettingsDialog(), + ); + }, ), ).paddedOnly(top: 4, bottom: 4), bottom: widget.bottom, diff --git a/lib/features/settings/view/dialogs/account_settings_dialog.dart b/lib/features/settings/view/dialogs/account_settings_dialog.dart new file mode 100644 index 0000000..530078b --- /dev/null +++ b/lib/features/settings/view/dialogs/account_settings_dialog.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; + +class AccountSettingsDialog extends StatelessWidget { + const AccountSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + scrollable: true, + contentPadding: EdgeInsets.zero, + icon: const PaperlessLogo.green(), + title: const Text(" Your Accounts"), + content: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + ExpansionTile( + leading: CircleAvatar( + child: Text(state.information?.username + ?.toUpperCase() + .substring(0, 1) ?? + ''), + ), + title: Text(state.information?.username ?? ''), + subtitle: Text(state.information?.host ?? ''), + children: const [ + HintCard( + hintText: "WIP: Coming soon with multi user support!", + ), + ], + ), + Divider(), + ListTile( + dense: true, + leading: const Icon(Icons.person_add_rounded), + title: const Text("Add another account"), //TODO: INTL + onTap: () {}, + ), + Divider(), + OutlinedButton( + child: Text( + S.of(context).appDrawerLogoutLabel, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onPressed: () async { + await _onLogout(context); + Navigator.of(context).maybePop(); + }, + ), + ], + ); + }, + ), + actions: [ + TextButton( + child: Text(S.of(context).genericActionCloseLabel), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } + + Future _onLogout(BuildContext context) async { + try { + await context.read().logout(); + await context.read().clear(); + await context.read>().clear(); + await context + .read>() + .clear(); + await context + .read>() + .clear(); + await context + .read>() + .clear(); + await context.read().clear(); + await HydratedBloc.storage.clear(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } +} diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index f29a4db..2fffad6 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -26,16 +26,6 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(S.of(context).appDrawerSettingsLabel), - actions: [ - IconButton( - icon: const Icon(Icons.logout), - color: Theme.of(context).colorScheme.error, - onPressed: () async { - await _onLogout(context); - Navigator.pop(context); - }, - ), - ], ), bottomNavigationBar: BlocBuilder( @@ -48,14 +38,16 @@ class SettingsPage extends StatelessWidget { " " + (info.username ?? 'unknown') + "@${info.host}", - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, ), subtitle: Text( S.of(context).serverInformationPaperlessVersionText + ' ' + info.version.toString() + ' (API v${info.apiVersion})', - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, ), ); }, @@ -100,25 +92,4 @@ class SettingsPage extends StatelessWidget { ), ); } - - Future _onLogout(BuildContext context) async { - try { - await context.read().logout(); - await context.read().clear(); - await context.read>().clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context.read().clear(); - await HydratedBloc.storage.clear(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } } diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 8751050..b83ca4b 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Ověř svou identitu", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 2f5c5e7..3c75572 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 54de836..a90f39b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 98d0d23..f9a6e61 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6cf6736..4f51e8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,6 +49,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_sta import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/constants.dart'; import 'package:path_provider/path_provider.dart'; @@ -254,6 +255,10 @@ class _PaperlessMobileEntrypointState extends State { GlobalWidgetsLocalizations.delegate, FormBuilderLocalizations.delegate, ], + routes: { + DocumentDetailsRoute.routeName: (context) => + const DocumentDetailsRoute(), + }, home: const AuthenticationWrapper(), ); }, diff --git a/lib/routes/document_details_route.dart b/lib/routes/document_details_route.dart new file mode 100644 index 0000000..36a0fad --- /dev/null +++ b/lib/routes/document_details_route.dart @@ -0,0 +1,46 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; + +class DocumentDetailsRoute extends StatelessWidget { + static const String routeName = "/documentDetails"; + const DocumentDetailsRoute({super.key}); + + @override + Widget build(BuildContext context) { + final args = ModalRoute.of(context)!.settings.arguments + as DocumentDetailsRouteArguments; + + return BlocProvider( + create: (context) => DocumentDetailsCubit( + context.read(), + args.document, + ), + child: LabelRepositoriesProvider( + child: DocumentDetailsPage( + allowEdit: args.allowEdit, + isLabelClickable: args.isLabelClickable, + titleAndContentQueryString: args.titleAndContentQueryString, + ), + ), + ); + } +} + +class DocumentDetailsRouteArguments { + final DocumentModel document; + final bool isLabelClickable; + final bool allowEdit; + final String? titleAndContentQueryString; + + DocumentDetailsRouteArguments({ + required this.document, + this.isLabelClickable = true, + this.allowEdit = true, + this.titleAndContentQueryString, + }); +} diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 2c547c5..42e9609 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -84,9 +84,11 @@ class DocumentModel extends Equatable { id: id, title: title ?? this.title, content: content ?? this.content, - documentType: documentType?.call() ?? this.documentType, - correspondent: correspondent?.call() ?? this.correspondent, - storagePath: storagePath?.call() ?? this.storagePath, + documentType: + documentType != null ? documentType.call() : this.documentType, + correspondent: + correspondent != null ? correspondent.call() : this.correspondent, + storagePath: storagePath != null ? storagePath.call() : this.storagePath, tags: tags ?? this.tags, created: created ?? this.created, modified: modified ?? this.modified, diff --git a/pubspec.lock b/pubspec.lock index 1157538..4bb00ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "12047baeca0e01df93165ef33275b32119d72699ab9a49dc64c20e78f586f96d" + url: "https://pub.dev" + source: hosted + version: "5.0.4" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: de5bfbc02ae4eebb339dd90d325749ae7536e903f6513ef72b88954072d72b0e + url: "https://pub.dev" + source: hosted + version: "5.0.3" badges: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c50f2f0..d10f818 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: responsive_builder: ^0.4.3 open_filex: ^4.3.2 dynamic_color: ^1.5.4 + auto_route: ^5.0.4 dev_dependencies: integration_test: @@ -102,6 +103,7 @@ dev_dependencies: flutter_lints: ^1.0.0 json_serializable: ^6.5.4 dart_code_metrics: ^5.4.0 + auto_route_generator: ^5.0.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec