Added search bar on all pages

This commit is contained in:
Anton Stubenbord
2023-02-01 00:27:56 +01:00
parent e9e9fdc336
commit 748cd21713
35 changed files with 1122 additions and 653 deletions

View File

@@ -1,217 +1,217 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; // import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/constants.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_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.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/model/view_type.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart'; // import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n.dart'; // import 'package:paperless_mobile/generated/l10n.dart';
import 'package:url_launcher/link.dart'; // import 'package:url_launcher/link.dart';
import 'package:url_launcher/url_launcher_string.dart'; // import 'package:url_launcher/url_launcher_string.dart';
/// Declares selectable actions in menu. // /// Declares selectable actions in menu.
enum AppPopupMenuEntries { // enum AppPopupMenuEntries {
// Documents preview // // Documents preview
documentsSelectListView, // documentsSelectListView,
documentsSelectGridView, // documentsSelectGridView,
// Generic actions // // Generic actions
openAboutThisAppDialog, // openAboutThisAppDialog,
reportBug, // reportBug,
openSettings, // openSettings,
// Adds a divider // // Adds a divider
divider; // divider;
} // }
class AppOptionsPopupMenu extends StatelessWidget { // class AppOptionsPopupMenu extends StatelessWidget {
final List<AppPopupMenuEntries> displayedActions; // final List<AppPopupMenuEntries> displayedActions;
const AppOptionsPopupMenu({ // const AppOptionsPopupMenu({
super.key, // super.key,
required this.displayedActions, // required this.displayedActions,
}); // });
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
return PopupMenuButton<AppPopupMenuEntries>( // return PopupMenuButton<AppPopupMenuEntries>(
position: PopupMenuPosition.under, // position: PopupMenuPosition.under,
icon: const Icon(Icons.more_vert), // icon: const Icon(Icons.more_vert),
onSelected: (action) { // onSelected: (action) {
switch (action) { // switch (action) {
case AppPopupMenuEntries.documentsSelectListView: // case AppPopupMenuEntries.documentsSelectListView:
context.read<ApplicationSettingsCubit>().setViewType(ViewType.list); // context.read<ApplicationSettingsCubit>().setViewType(ViewType.list);
break; // break;
case AppPopupMenuEntries.documentsSelectGridView: // case AppPopupMenuEntries.documentsSelectGridView:
context.read<ApplicationSettingsCubit>().setViewType(ViewType.grid); // context.read<ApplicationSettingsCubit>().setViewType(ViewType.grid);
break; // break;
case AppPopupMenuEntries.openAboutThisAppDialog: // case AppPopupMenuEntries.openAboutThisAppDialog:
_showAboutDialog(context); // _showAboutDialog(context);
break; // break;
case AppPopupMenuEntries.openSettings: // case AppPopupMenuEntries.openSettings:
Navigator.of(context).push( // Navigator.of(context).push(
MaterialPageRoute( // MaterialPageRoute(
builder: (context) => BlocProvider.value( // builder: (context) => BlocProvider.value(
value: context.read<ApplicationSettingsCubit>(), // value: context.read<ApplicationSettingsCubit>(),
child: const SettingsPage(), // child: const SettingsPage(),
), // ),
), // ),
); // );
break; // break;
case AppPopupMenuEntries.reportBug: // case AppPopupMenuEntries.reportBug:
launchUrlString( // launchUrlString(
'https://github.com/astubenbord/paperless-mobile/issues/new', // 'https://github.com/astubenbord/paperless-mobile/issues/new',
); // );
break; // break;
default: // default:
break; // break;
} // }
}, // },
itemBuilder: _buildEntries, // itemBuilder: _buildEntries,
); // );
} // }
PopupMenuItem<AppPopupMenuEntries> _buildReportBugTile(BuildContext context) { // PopupMenuItem<AppPopupMenuEntries> _buildReportBugTile(BuildContext context) {
return PopupMenuItem( // return PopupMenuItem(
value: AppPopupMenuEntries.reportBug, // value: AppPopupMenuEntries.reportBug,
padding: EdgeInsets.zero, // padding: EdgeInsets.zero,
child: ListTile( // child: ListTile(
leading: const Icon(Icons.bug_report), // leading: const Icon(Icons.bug_report),
title: Text(S.of(context).appDrawerReportBugLabel), // title: Text(S.of(context).appDrawerReportBugLabel),
), // ),
); // );
} // }
PopupMenuItem<AppPopupMenuEntries> _buildSettingsTile(BuildContext context) { // PopupMenuItem<AppPopupMenuEntries> _buildSettingsTile(BuildContext context) {
return PopupMenuItem( // return PopupMenuItem(
padding: EdgeInsets.zero, // padding: EdgeInsets.zero,
value: AppPopupMenuEntries.openSettings, // value: AppPopupMenuEntries.openSettings,
child: ListTile( // child: ListTile(
leading: const Icon(Icons.settings_outlined), // leading: const Icon(Icons.settings_outlined),
title: Text(S.of(context).appDrawerSettingsLabel), // title: Text(S.of(context).appDrawerSettingsLabel),
), // ),
); // );
} // }
PopupMenuItem<AppPopupMenuEntries> _buildAboutTile(BuildContext context) { // PopupMenuItem<AppPopupMenuEntries> _buildAboutTile(BuildContext context) {
return PopupMenuItem( // return PopupMenuItem(
padding: EdgeInsets.zero, // padding: EdgeInsets.zero,
value: AppPopupMenuEntries.openAboutThisAppDialog, // value: AppPopupMenuEntries.openAboutThisAppDialog,
child: ListTile( // child: ListTile(
leading: const Icon(Icons.info_outline), // leading: const Icon(Icons.info_outline),
title: Text(S.of(context).appDrawerAboutLabel), // title: Text(S.of(context).appDrawerAboutLabel),
), // ),
); // );
} // }
PopupMenuItem<AppPopupMenuEntries> _buildListViewTile() { // PopupMenuItem<AppPopupMenuEntries> _buildListViewTile() {
return PopupMenuItem( // return PopupMenuItem(
padding: EdgeInsets.zero, // padding: EdgeInsets.zero,
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>( // child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, state) { // builder: (context, state) {
return ListTile( // return ListTile(
leading: const Icon(Icons.list), // leading: const Icon(Icons.list),
title: const Text("List"), // title: const Text("List"),
trailing: state.preferredViewType == ViewType.list // trailing: state.preferredViewType == ViewType.list
? const Icon(Icons.check) // ? const Icon(Icons.check)
: null, // : null,
); // );
}, // },
), // ),
value: AppPopupMenuEntries.documentsSelectListView, // value: AppPopupMenuEntries.documentsSelectListView,
); // );
} // }
PopupMenuItem<AppPopupMenuEntries> _buildGridViewTile() { // PopupMenuItem<AppPopupMenuEntries> _buildGridViewTile() {
return PopupMenuItem( // return PopupMenuItem(
value: AppPopupMenuEntries.documentsSelectGridView, // value: AppPopupMenuEntries.documentsSelectGridView,
padding: EdgeInsets.zero, // padding: EdgeInsets.zero,
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>( // child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, state) { // builder: (context, state) {
return ListTile( // return ListTile(
leading: const Icon(Icons.grid_view_rounded), // leading: const Icon(Icons.grid_view_rounded),
title: const Text("Grid"), // title: const Text("Grid"),
trailing: state.preferredViewType == ViewType.grid // trailing: state.preferredViewType == ViewType.grid
? const Icon(Icons.check) // ? const Icon(Icons.check)
: null, // : null,
); // );
}, // },
), // ),
); // );
} // }
void _showAboutDialog(BuildContext context) { // void _showAboutDialog(BuildContext context) {
showAboutDialog( // showAboutDialog(
context: context, // context: context,
applicationIcon: const ImageIcon( // applicationIcon: const ImageIcon(
AssetImage('assets/logos/paperless_logo_green.png'), // AssetImage('assets/logos/paperless_logo_green.png'),
), // ),
applicationName: 'Paperless Mobile', // applicationName: 'Paperless Mobile',
applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, // applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber,
children: [ // children: [
Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), // Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')),
Link( // Link(
uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), // uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'),
builder: (context, followLink) => GestureDetector( // builder: (context, followLink) => GestureDetector(
onTap: followLink, // onTap: followLink,
child: Text( // child: Text(
'https://github.com/astubenbord/paperless-mobile', // 'https://github.com/astubenbord/paperless-mobile',
style: TextStyle(color: Theme.of(context).colorScheme.tertiary), // style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
), // ),
), // ),
), // ),
const SizedBox(height: 16), // const SizedBox(height: 16),
Text( // Text(
'Credits', // 'Credits',
style: Theme.of(context).textTheme.titleMedium, // style: Theme.of(context).textTheme.titleMedium,
), // ),
_buildOnboardingImageCredits(), // _buildOnboardingImageCredits(),
], // ],
); // );
} // }
Widget _buildOnboardingImageCredits() { // Widget _buildOnboardingImageCredits() {
return Link( // return Link(
uri: Uri.parse( // uri: Uri.parse(
'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), // '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( // builder: (context, followLink) => Wrap(
children: [ // children: [
const Text('Onboarding images by '), // const Text('Onboarding images by '),
GestureDetector( // GestureDetector(
onTap: followLink, // onTap: followLink,
child: Text( // child: Text(
'pch.vector', // 'pch.vector',
style: TextStyle(color: Theme.of(context).colorScheme.tertiary), // style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
), // ),
), // ),
const Text(' on Freepik.') // const Text(' on Freepik.')
], // ],
), // ),
); // );
} // }
List<PopupMenuEntry<AppPopupMenuEntries>> _buildEntries( // List<PopupMenuEntry<AppPopupMenuEntries>> _buildEntries(
BuildContext context) { // BuildContext context) {
List<PopupMenuEntry<AppPopupMenuEntries>> items = []; // List<PopupMenuEntry<AppPopupMenuEntries>> items = [];
for (final entry in displayedActions) { // for (final entry in displayedActions) {
switch (entry) { // switch (entry) {
case AppPopupMenuEntries.documentsSelectListView: // case AppPopupMenuEntries.documentsSelectListView:
items.add(_buildListViewTile()); // items.add(_buildListViewTile());
break; // break;
case AppPopupMenuEntries.documentsSelectGridView: // case AppPopupMenuEntries.documentsSelectGridView:
items.add(_buildGridViewTile()); // items.add(_buildGridViewTile());
break; // break;
case AppPopupMenuEntries.openAboutThisAppDialog: // case AppPopupMenuEntries.openAboutThisAppDialog:
items.add(_buildAboutTile(context)); // items.add(_buildAboutTile(context));
break; // break;
case AppPopupMenuEntries.reportBug: // case AppPopupMenuEntries.reportBug:
items.add(_buildReportBugTile(context)); // items.add(_buildReportBugTile(context));
break; // break;
case AppPopupMenuEntries.openSettings: // case AppPopupMenuEntries.openSettings:
items.add(_buildSettingsTile(context)); // items.add(_buildSettingsTile(context));
break; // break;
case AppPopupMenuEntries.divider: // case AppPopupMenuEntries.divider:
items.add(const PopupMenuDivider()); // items.add(const PopupMenuDivider());
break; // break;
} // }
} // }
return items; // return items;
} // }
} // }

View File

@@ -39,7 +39,7 @@ class SearchBar extends StatelessWidget {
surfaceTintColor: colorScheme.surfaceTint, surfaceTintColor: colorScheme.surfaceTint,
borderRadius: BorderRadius.circular(effectiveHeight / 2), borderRadius: BorderRadius.circular(effectiveHeight / 2),
child: InkWell( child: InkWell(
onTap: () {}, onTap: onTap,
borderRadius: BorderRadius.circular(effectiveHeight / 2), borderRadius: BorderRadius.circular(effectiveHeight / 2),
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
splashFactory: InkRipple.splashFactory, splashFactory: InkRipple.splashFactory,
@@ -51,7 +51,9 @@ class SearchBar extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: TextField( child: TextField(
onTap: onTap,
readOnly: true, readOnly: true,
enabled: false,
cursorColor: colorScheme.primary, cursorColor: colorScheme.primary,
style: textTheme.bodyLarge, style: textTheme.bodyLarge,
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
@@ -64,7 +66,6 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
), ),
onTap: onTap,
), ),
), ),
), ),

View File

@@ -2,18 +2,25 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
class PaperlessLogo extends StatelessWidget { class PaperlessLogo extends StatelessWidget {
static const _paperlessGreen = Color(0xFF18541F);
final double? height; final double? height;
final double? width; final double? width;
final String _path; final Color _color;
const PaperlessLogo.white({super.key, this.height, this.width}) const PaperlessLogo.white({
: _path = "assets/logos/paperless_logo_white.svg"; super.key,
this.height,
this.width,
}) : _color = Colors.white;
const PaperlessLogo.green({super.key, this.height, this.width}) 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}) 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -24,7 +31,8 @@ class PaperlessLogo extends StatelessWidget {
), ),
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: SvgPicture.asset( child: SvgPicture.asset(
_path, "assets/logos/paperless_logo_white.svg",
color: _color,
), ),
); );
} }

View File

@@ -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<ApplicationSettingsCubit>(),
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.')
],
),
);
}
}

View File

@@ -1,15 +1,10 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.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_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/file_service.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'; part 'document_details_state.dart';

View File

@@ -482,7 +482,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: _DetailsItem( child: _DetailsItem(
label: S.of(context).documentStoragePathPropertyLabel, label: S.of(context).documentStoragePathPropertyLabel,
content: StoragePathWidget( content: StoragePathWidget(
isClickable: widget.isLabelClickable,
pathId: document.storagePath, pathId: document.storagePath,
), ),
).paddedSymmetrically(vertical: 16), ).paddedSymmetrically(vertical: 16),

View File

@@ -1,8 +1,8 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.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/paged_document_view/paged_documents_mixin.dart';
import 'package:paperless_mobile/features/search/cubit/document_search_state.dart';
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState> class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
with PagedDocumentsMixin { with PagedDocumentsMixin {

View File

@@ -27,11 +27,8 @@ class DocumentSearchState extends PagedDocumentsState {
}); });
@override @override
List<Object> get props => [ List<Object?> get props => [
hasLoaded, ...super.props,
isLoading,
filter,
value,
searchHistory, searchHistory,
suggestions, suggestions,
view, view,

View File

@@ -1,11 +1,13 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/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/generated/l10n.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
Future<void> showDocumentSearchPage(BuildContext context) { Future<void> showDocumentSearchPage(BuildContext context) {
return Navigator.of(context).push( return Navigator.of(context).push(
@@ -151,12 +153,27 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
isLabelClickable: false, isLabelClickable: false,
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, 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<DocumentSearchCubit>().reload();
}
},
) )
], ],
); );
} }
void _selectSuggestion(String suggestion) { void _selectSuggestion(String suggestion) {
_queryController.text = suggestion;
context.read<DocumentSearchCubit>().search(suggestion); context.read<DocumentSearchCubit>().search(suggestion);
FocusScope.of(context).unfocus();
} }
} }

View File

@@ -12,10 +12,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
final SavedViewRepository _savedViewRepository; DocumentsCubit(this.api) : super(const DocumentsState());
DocumentsCubit(this.api, this._savedViewRepository)
: super(const DocumentsState());
Future<void> bulkRemove(List<DocumentModel> documents) async { Future<void> bulkRemove(List<DocumentModel> documents) async {
log("[DocumentsCubit] bulkRemove"); log("[DocumentsCubit] bulkRemove");

View File

@@ -1,33 +1,28 @@
import 'dart:developer';
import 'package:badges/badges.dart' as b; import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.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/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.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/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/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/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/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/add_saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.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/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_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.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/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class DocumentFilterIntent { class DocumentFilterIntent {
final DocumentFilter? filter; final DocumentFilter? filter;
@@ -116,6 +111,7 @@ class _DocumentsPageState extends State<DocumentsPage>
}, },
builder: (context, connectivityState) { builder: (context, connectivityState) {
return Scaffold( return Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>( floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount; final appliedFiltersCount = state.filter.appliedFiltersCount;
@@ -165,6 +161,7 @@ class _DocumentsPageState extends State<DocumentsPage>
context, context,
), ),
sliver: SearchAppBar( sliver: SearchAppBar(
hintText: "Search documents", //TODO: INTL
onOpenSearch: showDocumentSearchPage, onOpenSearch: showDocumentSearchPage,
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
@@ -177,9 +174,12 @@ class _DocumentsPageState extends State<DocumentsPage>
), ),
), ),
], ],
body: NotificationListener<ScrollUpdateNotification>( body: NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
final metrics = notification.metrics; final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
}
final desiredTab = final desiredTab =
(metrics.pixels / metrics.maxScrollExtent).round(); (metrics.pixels / metrics.maxScrollExtent).round();
if (metrics.axis == Axis.horizontal && if (metrics.axis == Axis.horizontal &&
@@ -414,29 +414,21 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
Future<void> _openDetails(DocumentModel document) async { Future<void> _openDetails(DocumentModel document) async {
final updatedModel = await Navigator.of(context).push<DocumentModel?>( final updatedModel = await Navigator.pushNamed(
_buildDetailsPageRoute(document), context,
); DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
),
) as DocumentModel?;
// final updatedModel = await Navigator.of(context).push<DocumentModel?>(
// _buildDetailsPageRoute(document),
// );
if (updatedModel != document) { if (updatedModel != document) {
context.read<DocumentsCubit>().reload(); context.read<DocumentsCubit>().reload();
} }
} }
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
DocumentModel document) {
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => DocumentDetailsCubit(
context.read<PaperlessDocumentsApi>(),
document,
),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(),
),
),
);
}
void _addTagToFilter(int tagId) { void _addTagToFilter(int tagId) {
try { try {
final tagsQuery = final tagsQuery =

View File

@@ -48,11 +48,18 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
int _currentIndex = 0; int _currentIndex = 0;
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit(); final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
late final InboxCubit _inboxCubit;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeData(context); _initializeData(context);
_inboxCubit = InboxCubit(
context.read(),
context.read(),
context.read(),
context.read(),
);
context.read<ConnectivityCubit>().reload(); context.read<ConnectivityCubit>().reload();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles(); _listenForReceivedFiles();
@@ -147,6 +154,12 @@ class _HomePageState extends State<HomePage> {
} }
} }
@override
void dispose() {
_inboxCubit.close();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final destinations = [ final destinations = [
@@ -182,28 +195,15 @@ class _HomePageState extends State<HomePage> {
), ),
label: S.of(context).bottomNavInboxPageLabel, 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 = <Widget>[ final routes = <Widget>[
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => DocumentsCubit( create: (context) => DocumentsCubit(context.read()),
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
),
), ),
BlocProvider( BlocProvider(
create: (context) => SavedViewCubit( create: (context) => SavedViewCubit(context.read()),
context.read<SavedViewRepository>(),
),
), ),
], ],
child: const DocumentsPage(), child: const DocumentsPage(),
@@ -213,13 +213,8 @@ class _HomePageState extends State<HomePage> {
child: const ScannerPage(), child: const ScannerPage(),
), ),
const LabelsPage(), const LabelsPage(),
BlocProvider( BlocProvider.value(
create: (context) => InboxCubit( value: _inboxCubit,
context.read(),
context.read(),
context.read(),
context.read(),
),
child: const InboxPage(), child: const InboxPage(),
), ),
// const SettingsPage(), // const SettingsPage(),
@@ -249,7 +244,6 @@ class _HomePageState extends State<HomePage> {
builder: (context, sizingInformation) { builder: (context, sizingInformation) {
if (!sizingInformation.isMobile) { if (!sizingInformation.isMobile) {
return Scaffold( return Scaffold(
// drawer: const AppDrawer(),
body: Row( body: Row(
children: [ children: [
NavigationRail( NavigationRail(

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.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/features/documents/view/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/dart_extensions.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/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_empty_widget.dart';
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.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/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -54,39 +57,8 @@ class _InboxPageState extends State<InboxPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const _progressBarHeight = 4.0;
return Scaffold( return Scaffold(
appBar: PreferredSize( drawer: const AppDrawer(),
preferredSize:
const Size.fromHeight(kToolbarHeight + _progressBarHeight),
child: BlocBuilder<InboxCubit, InboxState>(
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)
],
);
},
),
),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>( floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) { builder: (context, state) {
if (!state.hasLoaded || state.documents.isEmpty) { if (!state.hasLoaded || state.documents.isEmpty) {
@@ -104,91 +76,95 @@ class _InboxPageState extends State<InboxPage> {
); );
}, },
), ),
body: BlocBuilder<InboxCubit, InboxState>( body: NestedScrollView(
builder: (context, state) { headerSliverBuilder: (context, innerBoxIsScrolled) => [
if (!state.hasLoaded) { SearchAppBar(
return const DocumentsListLoadingWidget(); hintText: "Search documents",
} onOpenSearch: showDocumentSearchPage,
),
],
body: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
if (!state.hasLoaded) {
return const CustomScrollView(
physics: NeverScrollableScrollPhysics(),
slivers: [DocumentsListLoadingWidget()],
);
}
if (state.documents.isEmpty) { if (state.documents.isEmpty) {
return InboxEmptyWidget( return InboxEmptyWidget(
emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey,
); );
} }
// Build a list of slivers alternating between SliverToBoxAdapter // Build a list of slivers alternating between SliverToBoxAdapter
// (group header) and a SliverList (inbox items). // (group header) and a SliverList (inbox items).
final List<Widget> slivers = _groupByDate(state.documents) final List<Widget> slivers = _groupByDate(state.documents)
.entries .entries
.map( .map(
(entry) => [ (entry) => [
SliverToBoxAdapter( SliverToBoxAdapter(
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32.0),
child: Text( child: Text(
entry.key, entry.key,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).padded(), ).padded(),
), ),
).paddedOnly(top: 8.0), ).paddedOnly(top: 8.0),
), ),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
childCount: entry.value.length, childCount: entry.value.length,
(context, index) { (context, index) {
if (index < entry.value.length - 1) { if (index < entry.value.length - 1) {
return Column( return Column(
children: [ children: [
_buildListItem( _buildListItem(
entry.value[index], entry.value[index],
), ),
const Divider( const Divider(
indent: 16, indent: 16,
endIndent: 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<InboxCubit>().initializeInbox(),
child: CustomScrollView(
controller: _scrollController,
slivers: [
SliverToBoxAdapter(
child: HintCard(
show: !state.isHintAcknowledged,
hintText: S.of(context).inboxPageUsageHintText,
onHintAcknowledged: () =>
context.read<InboxCubit>().acknowledgeHint(),
), ),
), ),
...slivers,
], ],
) ),
.flattened );
.toList() },
..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); ),
return RefreshIndicator(
onRefresh: () => context.read<InboxCubit>().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<InboxCubit>().acknowledgeHint(),
),
),
...slivers,
],
),
),
],
),
);
},
), ),
); );
} }

View File

@@ -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/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class InboxItem extends StatefulWidget { class InboxItem extends StatefulWidget {
static const _a4AspectRatio = 1 / 1.4142; static const _a4AspectRatio = 1 / 1.4142;
@@ -40,24 +41,16 @@ class _InboxItemState extends State<InboxItem> {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () async { onTap: () async {
final returnedDocument = await Navigator.push<DocumentModel?>( final updatedDocument = await Navigator.pushNamed(
context, context,
MaterialPageRoute( DocumentDetailsRoute.routeName,
builder: (context) => BlocProvider( arguments: DocumentDetailsRouteArguments(
create: (context) => DocumentDetailsCubit( document: widget.document,
context.read<PaperlessDocumentsApi>(), isLabelClickable: false,
widget.document,
),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(
isLabelClickable: false,
),
),
),
), ),
); ) as DocumentModel?;
if (returnedDocument != null) { if (updatedDocument != null) {
widget.onDocumentUpdated(returnedDocument); widget.onDocumentUpdated(updatedDocument);
} }
}, },
child: SizedBox( child: SizedBox(

View File

@@ -266,6 +266,10 @@ class _TagFormFieldState extends State<TagFormField> {
Widget _buildNotAssignedTag(FormFieldState<TagsQuery> field) { Widget _buildNotAssignedTag(FormFieldState<TagsQuery> field) {
return ColoredChipWrapper( return ColoredChipWrapper(
child: InputChip( child: InputChip(
labelPadding: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.all(4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide.none,
label: Text( label: Text(
S.of(context).labelNotAssignedText, S.of(context).labelNotAssignedText,
), ),
@@ -288,6 +292,10 @@ class _TagFormFieldState extends State<TagFormField> {
} }
return ColoredChipWrapper( return ColoredChipWrapper(
child: InputChip( child: InputChip(
labelPadding: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.all(4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide.none,
label: Text( label: Text(
tag.name, tag.name,
style: TextStyle( style: TextStyle(
@@ -312,6 +320,10 @@ class _TagFormFieldState extends State<TagFormField> {
Widget _buildAnyAssignedTag(FormFieldState<TagsQuery> field) { Widget _buildAnyAssignedTag(FormFieldState<TagsQuery> field) {
return ColoredChipWrapper( return ColoredChipWrapper(
child: InputChip( 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), label: Text(S.of(context).labelAnyAssignedText),
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12), Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12),

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -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/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_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/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_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
@@ -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_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_storage_path_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_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/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/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/storage_path_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_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/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'; import 'package:paperless_mobile/generated/l10n.dart';
class LabelsPage extends StatefulWidget { class LabelsPage extends StatefulWidget {
@@ -51,154 +57,264 @@ class _LabelsPageState extends State<LabelsPage>
child: BlocBuilder<ConnectivityCubit, ConnectivityState>( child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) { builder: (context, connectedState) {
return Scaffold( return Scaffold(
appBar: AppBar( drawer: const AppDrawer(),
title: Text( floatingActionButton: FloatingActionButton(
[ onPressed: [
S.of(context).labelsPageCorrespondentsTitleText, _openAddCorrespondentPage,
S.of(context).labelsPageDocumentTypesTitleText, _openAddDocumentTypePage,
S.of(context).labelsPageTagsTitleText, _openAddTagPage,
S.of(context).labelsPageStoragePathTitleText _openAddStoragePathPage,
][_currentIndex], ][_currentIndex],
), child: Icon(Icons.add),
actions: [ ),
IconButton( body: NestedScrollView(
onPressed: [ floatHeaderSlivers: true,
_openAddCorrespondentPage, headerSliverBuilder: (context, innerBoxIsScrolled) => [
_openAddDocumentTypePage, SliverOverlapAbsorber(
_openAddTagPage, // This widget takes the overlapping behavior of the SliverAppBar,
_openAddStoragePathPage, // and redirects it to the SliverOverlapInjector below. If it is
][_currentIndex], // missing, then it is possible for the nested "inner" scroll view
icon: const Icon(Icons.add), // 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( body: NotificationListener<ScrollNotification>(
preferredSize: Size.fromHeight( onNotification: (notification) {
kToolbarHeight + (!connectedState.isConnected ? 16 : 0)), final metrics = notification.metrics;
child: Column( if (metrics.maxScrollExtent == 0) {
children: [ return true;
if (!connectedState.isConnected) const OfflineBanner(), }
ColoredBox( final desiredTab =
color: Theme.of(context).bottomAppBarColor, ((metrics.pixels / metrics.maxScrollExtent) *
child: TabBar( (_tabController.length - 1))
indicatorColor: Theme.of(context).colorScheme.primary, .round();
controller: _tabController,
tabs: [ if (metrics.axis == Axis.horizontal &&
Tab( _currentIndex != desiredTab) {
icon: Icon( setState(() => _currentIndex = desiredTab);
Icons.person_outline, }
color: Theme.of(context) return true;
.colorScheme },
.onPrimaryContainer, child: MultiBlocProvider(
), providers: [
), BlocProvider(
Tab( create: (context) => LabelCubit<Correspondent>(
icon: Icon( context.read<
Icons.description_outlined, LabelRepository<Correspondent,
color: Theme.of(context) CorrespondentRepositoryState>>(),
.colorScheme ),
.onPrimaryContainer, ),
), BlocProvider(
), create: (context) => LabelCubit<DocumentType>(
Tab( context.read<
icon: Icon( LabelRepository<DocumentType,
Icons.label_outline, DocumentTypeRepositoryState>>(),
color: Theme.of(context) ),
.colorScheme ),
.onPrimaryContainer, BlocProvider(
), create: (context) => LabelCubit<Tag>(
), context
Tab( .read<LabelRepository<Tag, TagRepositoryState>>(),
icon: Icon( ),
Icons.folder_open, ),
color: Theme.of(context) BlocProvider(
.colorScheme create: (context) => LabelCubit<StoragePath>(
.onPrimaryContainer, context.read<
), LabelRepository<StoragePath,
) StoragePathRepositoryState>>(),
],
), ),
), ),
], ],
child: RefreshIndicator(
edgeOffset: kToolbarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () => [
context.read<LabelCubit<Correspondent>>(),
context.read<LabelCubit<DocumentType>>(),
context.read<LabelCubit<Tag>>(),
context.read<LabelCubit<StoragePath>>(),
][_currentIndex]
.reload(),
child: TabBarView(
controller: _tabController,
children: [
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
LabelTabView<Correspondent>(
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<DocumentType>(
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<Tag>(
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag ?? false
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
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<StoragePath>(
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<Correspondent>(
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<DocumentType>(
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<Tag>(
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag ?? false
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel:
S.of(context).labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription:
S.of(context).labelsPageTagsEmptyStateDescriptionText,
onAddNew: _openAddTagPage,
),
),
StoragePathBlocProvider(
child: LabelTabView<StoragePath>(
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,
),
),
],
),
); );
}, },
), ),

View File

@@ -46,45 +46,46 @@ class LabelTabView<T extends Label> extends StatelessWidget {
} }
final labels = state.labels.values.toList()..sort(); final labels = state.labels.values.toList()..sort();
if (labels.isEmpty) { if (labels.isEmpty) {
return Center( return SliverFillRemaining(
child: Column( child: Center(
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.center,
Text( children: [
emptyStateDescription, Text(
textAlign: TextAlign.center, emptyStateDescription,
), textAlign: TextAlign.center,
TextButton( ),
onPressed: onAddNew, TextButton(
child: Text(emptyStateActionButtonLabel), onPressed: onAddNew,
), child: Text(emptyStateActionButtonLabel),
].padded(), ),
].padded(),
),
), ),
); );
} }
return RefreshIndicator( return SliverList(
onRefresh: context.read<LabelCubit<T>>().reload, delegate: SliverChildBuilderDelegate(
notificationPredicate: (notification) => (context, index) {
connectivityState.isConnected, final l = labels.elementAt(index);
child: ListView( return LabelItem<T>(
children: labels name: l.name,
.map((l) => LabelItem<T>( content: contentBuilder?.call(l) ??
name: l.name, Text(
content: contentBuilder?.call(l) ?? translateMatchingAlgorithmName(
Text( context, l.matchingAlgorithm) +
translateMatchingAlgorithmName( ((l.match?.isNotEmpty ?? false)
context, l.matchingAlgorithm) + ? ": ${l.match}"
((l.match?.isNotEmpty ?? false) : ""),
? ": ${l.match}" maxLines: 2,
: ""), ),
maxLines: 2, onOpenEditPage: onEdit,
), filterBuilder: filterBuilder,
onOpenEditPage: onEdit, leading: leadingBuilder?.call(l),
filterBuilder: filterBuilder, label: l,
leading: leadingBuilder?.call(l), );
label: l, },
)) childCount: labels.length,
.toList(),
), ),
); );
}, },

View File

@@ -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/features/linked_documents/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class LinkedDocumentsPage extends StatefulWidget { class LinkedDocumentsPage extends StatefulWidget {
const LinkedDocumentsPage({super.key}); const LinkedDocumentsPage({super.key});
@@ -59,6 +60,19 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
isLabelClickable: false, isLabelClickable: false,
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, 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<LinkedDocumentsCubit>().reload();
}
},
); );
}, },
); );

View File

@@ -29,7 +29,8 @@ class SavedViewList extends StatelessWidget {
return ListTile( return ListTile(
title: Text(view.name), title: Text(view.name),
subtitle: Text( subtitle: Text(
"${view.filterRules.length} filter(s) set"), //TODO: INTL w/ placeholder "${view.filterRules.length} filter(s) set",
), //TODO: INTL w/ placeholder
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(

View File

@@ -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/saved_view/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class SavedViewPage extends StatefulWidget { class SavedViewPage extends StatefulWidget {
final Future<void> Function(SavedView savedView) onDelete; final Future<void> Function(SavedView savedView) onDelete;
@@ -101,6 +102,12 @@ class _SavedViewPageState extends State<SavedViewPage> {
onTap: _onOpenDocumentDetails, onTap: _onOpenDocumentDetails,
viewType: _viewType, viewType: _viewType,
), ),
if (state.hasLoaded && state.isLoading)
const SliverToBoxAdapter(
child: Center(
child: CircularProgressIndicator(),
),
)
], ],
); );
}, },
@@ -111,20 +118,14 @@ class _SavedViewPageState extends State<SavedViewPage> {
} }
void _onOpenDocumentDetails(DocumentModel document) async { void _onOpenDocumentDetails(DocumentModel document) async {
final updatedDocument = await Navigator.push<DocumentModel>( final updatedDocument = await Navigator.pushNamed(
context, context,
MaterialPageRoute( DocumentDetailsRoute.routeName,
builder: (_) => BlocProvider( arguments: DocumentDetailsRouteArguments(
create: (context) => DocumentDetailsCubit( document: document,
context.read<PaperlessDocumentsApi>(), isLabelClickable: false,
document,
),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(),
),
),
), ),
); ) as DocumentModel?;
if (updatedDocument != document) { if (updatedDocument != document) {
// Reload in case document was edited and might not fulfill filter criteria of saved view anymore // Reload in case document was edited and might not fulfill filter criteria of saved view anymore
context.read<SavedViewDetailsCubit>().reload(); context.read<SavedViewDetailsCubit>().reload();

View File

@@ -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/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.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/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.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/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/widgets/scanned_image_item.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/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/file_helpers.dart'; import 'package:paperless_mobile/helpers/file_helpers.dart';
@@ -47,20 +49,100 @@ class _ScannerPageState extends State<ScannerPage>
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) { builder: (context, connectedState) {
return Scaffold( return Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _openDocumentScanner(context), onPressed: () => _openDocumentScanner(context),
child: const Icon(Icons.add_a_photo_outlined), child: const Icon(Icons.add_a_photo_outlined),
), ),
appBar: _buildAppBar(context, connectedState.isConnected), //appBar: _buildAppBar(context, connectedState.isConnected),
body: Padding( // body: Padding(
padding: const EdgeInsets.all(8.0), // padding: const EdgeInsets.all(8.0),
child: _buildBody(connectedState.isConnected), // child: _buildBody(connectedState.isConnected),
// ),
body: BlocBuilder<DocumentScannerCubit, List<File>>(
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<DocumentScannerCubit, List<File>>(
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<DocumentScannerCubit, List<File>>(
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<DocumentScannerCubit, List<File>>(
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) { AppBar _buildAppBar(BuildContext context, bool isConnected) {
return AppBar( return AppBar(
title: Text(S.of(context).documentScannerPageTitle), title: Text(S.of(context).documentScannerPageTitle),
@@ -170,7 +252,7 @@ class _ScannerPageState extends State<ScannerPage>
} }
} }
Widget _buildBody(bool isConnected) { Widget _buildEmptyState(bool isConnected) {
return BlocBuilder<DocumentScannerCubit, List<File>>( return BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, scans) { builder: (context, scans) {
if (scans.isNotEmpty) { if (scans.isNotEmpty) {
@@ -207,7 +289,7 @@ class _ScannerPageState extends State<ScannerPage>
} }
Widget _buildImageGrid(List<File> scans) { Widget _buildImageGrid(List<File> scans) {
return GridView.builder( return SliverGrid.builder(
itemCount: scans.length, itemCount: scans.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
typedef OpenSearchCallback = void Function(BuildContext context); typedef OpenSearchCallback = void Function(BuildContext context);
@@ -8,11 +9,13 @@ class SearchAppBar extends StatefulWidget with PreferredSizeWidget {
final PreferredSizeWidget? bottom; final PreferredSizeWidget? bottom;
final OpenSearchCallback onOpenSearch; final OpenSearchCallback onOpenSearch;
final Color? backgroundColor; final Color? backgroundColor;
final String hintText;
const SearchAppBar({ const SearchAppBar({
super.key, super.key,
required this.onOpenSearch, required this.onOpenSearch,
this.bottom, this.bottom,
this.backgroundColor, this.backgroundColor,
required this.hintText,
}); });
@override @override
@@ -26,25 +29,29 @@ class _SearchAppBarState extends State<SearchAppBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverAppBar( return SliverAppBar(
automaticallyImplyLeading: false,
floating: true, floating: true,
pinned: true, pinned: true,
snap: true, snap: true,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
title: SearchBar( title: SearchBar(
height: kToolbarHeight - 8, height: kToolbarHeight - 8,
supportingText: "Search documents", supportingText: widget.hintText,
onTap: () => widget.onOpenSearch(context), onTap: () => widget.onOpenSearch(context),
leadingIcon: IconButton( leadingIcon: IconButton(
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
onPressed: () { onPressed: Scaffold.of(context).openDrawer,
Scaffold.of(context).openDrawer();
},
), ),
trailingIcon: IconButton( trailingIcon: IconButton(
icon: const CircleAvatar( icon: const CircleAvatar(
child: Text("A"), child: Text("A"),
), ),
onPressed: () {}, onPressed: () {
showDialog(
context: context,
builder: (context) => AccountSettingsDialog(),
);
},
), ),
).paddedOnly(top: 4, bottom: 4), ).paddedOnly(top: 4, bottom: 4),
bottom: widget.bottom, bottom: widget.bottom,

View File

@@ -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<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
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<void> _onLogout(BuildContext context) async {
try {
await context.read<AuthenticationCubit>().logout();
await context.read<ApplicationSettingsCubit>().clear();
await context.read<LabelRepository<Tag, TagRepositoryState>>().clear();
await context
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>()
.clear();
await context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
.clear();
await context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
.clear();
await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -26,16 +26,6 @@ class SettingsPage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(S.of(context).appDrawerSettingsLabel), 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<PaperlessServerInformationCubit, bottomNavigationBar: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>( PaperlessServerInformationState>(
@@ -48,14 +38,16 @@ class SettingsPage extends StatelessWidget {
" " + " " +
(info.username ?? 'unknown') + (info.username ?? 'unknown') +
"@${info.host}", "@${info.host}",
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.labelSmall,
textAlign: TextAlign.center,
), ),
subtitle: Text( subtitle: Text(
S.of(context).serverInformationPaperlessVersionText + S.of(context).serverInformationPaperlessVersionText +
' ' + ' ' +
info.version.toString() + info.version.toString() +
' (API v${info.apiVersion})', ' (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<void> _onLogout(BuildContext context) async {
try {
await context.read<AuthenticationCubit>().logout();
await context.read<ApplicationSettingsCubit>().clear();
await context.read<LabelRepository<Tag, TagRepositoryState>>().clear();
await context
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>()
.clear();
await context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
.clear();
await context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
.clear();
await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
} }

View File

@@ -627,5 +627,6 @@
"verifyIdentityPageTitle": "Ověř svou identitu", "verifyIdentityPageTitle": "Ověř svou identitu",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"genericActionCloseLabel": "Close"
} }

View File

@@ -627,5 +627,6 @@
"verifyIdentityPageTitle": "Verifiziere deine Identität", "verifyIdentityPageTitle": "Verifiziere deine Identität",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"genericActionCloseLabel": "Close"
} }

View File

@@ -627,5 +627,6 @@
"verifyIdentityPageTitle": "Verify your identity", "verifyIdentityPageTitle": "Verify your identity",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"genericActionCloseLabel": "Close"
} }

View File

@@ -627,5 +627,6 @@
"verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "verifyIdentityPageTitle": "Kimliğinizi doğrulayın",
"@verifyIdentityPageTitle": {}, "@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula",
"@verifyIdentityPageVerifyIdentityButtonLabel": {} "@verifyIdentityPageVerifyIdentityButtonLabel": {},
"genericActionCloseLabel": "Close"
} }

View File

@@ -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/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.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/theme.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -254,6 +255,10 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate, FormBuilderLocalizations.delegate,
], ],
routes: {
DocumentDetailsRoute.routeName: (context) =>
const DocumentDetailsRoute(),
},
home: const AuthenticationWrapper(), home: const AuthenticationWrapper(),
); );
}, },

View File

@@ -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<PaperlessDocumentsApi>(),
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,
});
}

View File

@@ -84,9 +84,11 @@ class DocumentModel extends Equatable {
id: id, id: id,
title: title ?? this.title, title: title ?? this.title,
content: content ?? this.content, content: content ?? this.content,
documentType: documentType?.call() ?? this.documentType, documentType:
correspondent: correspondent?.call() ?? this.correspondent, documentType != null ? documentType.call() : this.documentType,
storagePath: storagePath?.call() ?? this.storagePath, correspondent:
correspondent != null ? correspondent.call() : this.correspondent,
storagePath: storagePath != null ? storagePath.call() : this.storagePath,
tags: tags ?? this.tags, tags: tags ?? this.tags,
created: created ?? this.created, created: created ?? this.created,
modified: modified ?? this.modified, modified: modified ?? this.modified,

View File

@@ -65,6 +65,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.10.0" 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: badges:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -88,6 +88,7 @@ dependencies:
responsive_builder: ^0.4.3 responsive_builder: ^0.4.3
open_filex: ^4.3.2 open_filex: ^4.3.2
dynamic_color: ^1.5.4 dynamic_color: ^1.5.4
auto_route: ^5.0.4
dev_dependencies: dev_dependencies:
integration_test: integration_test:
@@ -102,6 +103,7 @@ dev_dependencies:
flutter_lints: ^1.0.0 flutter_lints: ^1.0.0
json_serializable: ^6.5.4 json_serializable: ^6.5.4
dart_code_metrics: ^5.4.0 dart_code_metrics: ^5.4.0
auto_route_generator: ^5.0.3
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec