mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 13:15:55 -06:00
WIP - Reimplemented document search
This commit is contained in:
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
String translateSortField(BuildContext context, SortField sortField) {
|
||||
String translateSortField(BuildContext context, SortField? sortField) {
|
||||
switch (sortField) {
|
||||
case SortField.archiveSerialNumber:
|
||||
return S.of(context).documentArchiveSerialNumberPropertyShortLabel;
|
||||
@@ -18,5 +18,7 @@ String translateSortField(BuildContext context, SortField sortField) {
|
||||
return S.of(context).documentAddedPropertyLabel;
|
||||
case SortField.modified:
|
||||
return S.of(context).documentModifiedPropertyLabel;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
217
lib/core/widgets/app_options_popup_menu.dart
Normal file
217
lib/core/widgets/app_options_popup_menu.dart
Normal file
@@ -0,0 +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';
|
||||
|
||||
/// 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<AppPopupMenuEntries> displayedActions;
|
||||
const AppOptionsPopupMenu({
|
||||
super.key,
|
||||
required this.displayedActions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<AppPopupMenuEntries>(
|
||||
position: PopupMenuPosition.under,
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (action) {
|
||||
switch (action) {
|
||||
case AppPopupMenuEntries.documentsSelectListView:
|
||||
context.read<ApplicationSettingsCubit>().setViewType(ViewType.list);
|
||||
break;
|
||||
case AppPopupMenuEntries.documentsSelectGridView:
|
||||
context.read<ApplicationSettingsCubit>().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<ApplicationSettingsCubit>(),
|
||||
child: const SettingsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case AppPopupMenuEntries.reportBug:
|
||||
launchUrlString(
|
||||
'https://github.com/astubenbord/paperless-mobile/issues/new',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: _buildEntries,
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<AppPopupMenuEntries> _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<AppPopupMenuEntries> _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<AppPopupMenuEntries> _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<AppPopupMenuEntries> _buildListViewTile() {
|
||||
return PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
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<AppPopupMenuEntries> _buildGridViewTile() {
|
||||
return PopupMenuItem(
|
||||
value: AppPopupMenuEntries.documentsSelectGridView,
|
||||
padding: EdgeInsets.zero,
|
||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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<PopupMenuEntry<AppPopupMenuEntries>> _buildEntries(
|
||||
BuildContext context) {
|
||||
List<PopupMenuEntry<AppPopupMenuEntries>> 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;
|
||||
}
|
||||
}
|
||||
@@ -28,51 +28,48 @@ class SearchBar extends StatelessWidget {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: 360, maxWidth: 720),
|
||||
width: double.infinity,
|
||||
height: effectiveHeight,
|
||||
child: Material(
|
||||
elevation: 3,
|
||||
color: colorScheme.surface,
|
||||
shadowColor: colorScheme.shadow,
|
||||
surfaceTintColor: colorScheme.surfaceTint,
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minWidth: 360, maxWidth: 720),
|
||||
width: double.infinity,
|
||||
height: effectiveHeight,
|
||||
child: Material(
|
||||
elevation: 1,
|
||||
color: colorScheme.surface,
|
||||
shadowColor: colorScheme.shadow,
|
||||
surfaceTintColor: colorScheme.surfaceTint,
|
||||
borderRadius: BorderRadius.circular(effectiveHeight / 2),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: BorderRadius.circular(effectiveHeight / 2),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: BorderRadius.circular(effectiveHeight / 2),
|
||||
highlightColor: Colors.transparent,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(children: [
|
||||
leadingIcon,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: TextField(
|
||||
cursorColor: colorScheme.primary,
|
||||
style: textTheme.bodyLarge,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
hintText: supportingText,
|
||||
hintStyle: textTheme.bodyLarge?.apply(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
highlightColor: Colors.transparent,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(children: [
|
||||
leadingIcon,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
cursorColor: colorScheme.primary,
|
||||
style: textTheme.bodyLarge,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
hintText: supportingText,
|
||||
hintStyle: textTheme.bodyLarge?.apply(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
if (trailingIcon != null) trailingIcon!,
|
||||
]),
|
||||
),
|
||||
),
|
||||
if (trailingIcon != null) trailingIcon!,
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
|
||||
|
||||
import 'document_search_state.dart';
|
||||
|
||||
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
with DocumentsPagingMixin {
|
||||
////
|
||||
DocumentSearchCubit(this.api) : super(const DocumentSearchState());
|
||||
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
///
|
||||
/// Requests results based on [query] and adds [query] to the
|
||||
/// search history, removing old occurrences and trimming the list to
|
||||
/// the last 5 searches.
|
||||
///
|
||||
Future<void> updateResults(String query) async {
|
||||
await updateFilter(
|
||||
filter: state.filter.copyWith(query: TextQuery.titleAndContent(query)),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
searchHistory: [
|
||||
query,
|
||||
...state.searchHistory.where((element) => element != query)
|
||||
].take(5).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void removeHistoryEntry(String suggestion) {
|
||||
emit(state.copyWith(
|
||||
searchHistory: state.searchHistory
|
||||
.whereNot((element) => element == suggestion)
|
||||
.toList(),
|
||||
));
|
||||
}
|
||||
|
||||
Future<List<String>> findSuggestions(String query) {
|
||||
return api.autocomplete(query);
|
||||
}
|
||||
|
||||
@override
|
||||
DocumentSearchState? fromJson(Map<String, dynamic> json) {
|
||||
return DocumentSearchState.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(DocumentSearchState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import 'package:flutter/material.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/core/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.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/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/list/document_list_item.dart';
|
||||
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart'
|
||||
as m3;
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class DocumentSearchDelegate extends m3.SearchDelegate<DocumentModel> {
|
||||
final DocumentSearchCubit bloc;
|
||||
DocumentSearchDelegate(
|
||||
this.bloc, {
|
||||
required String hintText,
|
||||
required super.searchFieldStyle,
|
||||
}) : super(
|
||||
searchFieldLabel: hintText,
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.search,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) => const BackButton();
|
||||
|
||||
@override
|
||||
PreferredSizeWidget buildBottom(BuildContext context) => PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Divider(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
if (query.isEmpty) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
S.of(context).documentSearchHistory,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
).padded(16),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final label = state.searchHistory[index];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: Text(label),
|
||||
onTap: () => _onSuggestionSelected(
|
||||
context,
|
||||
label,
|
||||
),
|
||||
onLongPress: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(label),
|
||||
content: Text(
|
||||
S.of(context).documentSearchPageRemoveFromHistory,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
S.of(context).genericActionCancelLabel,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
S.of(context).genericActionDeleteLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
bloc.removeHistoryEntry(label);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: state.searchHistory.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return FutureBuilder<List<String>>(
|
||||
future: bloc.findSuggestions(query),
|
||||
builder: (context, snapshot) {
|
||||
final historyMatches = state.searchHistory
|
||||
.where((e) => e.startsWith(query))
|
||||
.toList();
|
||||
final serverSuggestions = (snapshot.data ?? [])
|
||||
..removeWhere((e) => historyMatches.contains(e));
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
S.of(context).documentSearchResults,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
).padded(),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => ListTile(
|
||||
title: Text(historyMatches[index]),
|
||||
leading: const Icon(Icons.history),
|
||||
onTap: () => _onSuggestionSelected(
|
||||
context,
|
||||
historyMatches[index],
|
||||
),
|
||||
),
|
||||
childCount: historyMatches.length,
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => ListTile(
|
||||
title: Text(serverSuggestions[index]),
|
||||
leading: const Icon(Icons.search),
|
||||
onTap: () => _onSuggestionSelected(
|
||||
context, snapshot.data![index]),
|
||||
),
|
||||
childCount: serverSuggestions.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onSuggestionSelected(BuildContext context, String suggestion) {
|
||||
query = suggestion;
|
||||
bloc.updateResults(query);
|
||||
super.showResults(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
return BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
if (!state.hasLoaded && state.isLoading) {
|
||||
return const DocumentsListLoadingWidget();
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: state.documents.length,
|
||||
itemBuilder: (context, index) => DocumentListItem(
|
||||
document: state.documents[index],
|
||||
onTap: (document) {
|
||||
Navigator.push<DocumentModel?>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => DocumentDetailsCubit(
|
||||
context.read<PaperlessDocumentsApi>(),
|
||||
document,
|
||||
),
|
||||
child: const LabelRepositoriesProvider(
|
||||
child: DocumentDetailsPage(
|
||||
isLabelClickable: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
).paddedSymmetrically(horizontal: 16),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
super.showSuggestions(context);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart';
|
||||
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_search/document_search_delegate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DocumentSearchBar extends StatelessWidget {
|
||||
const DocumentSearchBar({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
onTap: () => showMaterial3Search(
|
||||
context: context,
|
||||
delegate: DocumentSearchDelegate(
|
||||
DocumentSearchCubit(context.read()),
|
||||
searchFieldStyle: Theme.of(context).textTheme.bodyLarge,
|
||||
hintText: "Search documents",
|
||||
),
|
||||
),
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search documents",
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(56),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 48),
|
||||
),
|
||||
// title: Text(
|
||||
// "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||
|
||||
class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
with DocumentsPagingMixin {
|
||||
with PagedDocumentsMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
|
||||
class DocumentsState extends DocumentsPagedState {
|
||||
class DocumentsState extends PagedDocumentsState {
|
||||
final int? selectedSavedViewId;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
|
||||
@@ -5,8 +5,10 @@ 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/core/translation/sort_field_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/widgets/app_options_popup_menu.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.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/document_search/cubit/document_search_cubit.dart';
|
||||
@@ -25,6 +27,7 @@ import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_prov
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
|
||||
import 'package:paperless_mobile/features/search/view/document_search_page.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';
|
||||
@@ -32,7 +35,6 @@ import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
|
||||
class DocumentFilterIntent {
|
||||
final DocumentFilter? filter;
|
||||
@@ -137,142 +139,151 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
}
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
const linearProgressIndicatorHeight = 4.0;
|
||||
return Scaffold(
|
||||
drawer: BlocProvider.value(
|
||||
value: context.read<AuthenticationCubit>(),
|
||||
child: AppDrawer(
|
||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
||||
),
|
||||
),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(
|
||||
kToolbarHeight,
|
||||
),
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isEmpty) {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: true,
|
||||
title: Text(S.of(context).documentsPageTitle +
|
||||
" (${formatMaxCount(state.documents.length)})"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
showMaterial3Search(
|
||||
context: context,
|
||||
delegate: DocumentSearchDelegate(
|
||||
DocumentSearchCubit(context.read()),
|
||||
searchFieldStyle:
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
hintText: "Search documents",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SortDocumentsButton(),
|
||||
BlocBuilder<ApplicationSettingsCubit,
|
||||
ApplicationSettingsState>(
|
||||
builder: (context, settingsState) => IconButton(
|
||||
icon: Icon(
|
||||
settingsState.preferredViewType == ViewType.grid
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
),
|
||||
onPressed: () {
|
||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
||||
setState(() {
|
||||
_offset = 0;
|
||||
_last = 0;
|
||||
});
|
||||
final cubit =
|
||||
context.read<ApplicationSettingsCubit>();
|
||||
cubit.setViewType(
|
||||
cubit.state.preferredViewType.toggle());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () =>
|
||||
context.read<DocumentsCubit>().resetSelection(),
|
||||
),
|
||||
title: Text(
|
||||
'${state.selection.length} ${S.of(context).documentsSelectedText}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(context, state),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
return b.Badge(
|
||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||
showBadge: appliedFiltersCount > 0,
|
||||
badgeContent: Text(
|
||||
'$appliedFiltersCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
animationType: b.BadgeAnimationType.fade,
|
||||
badgeColor: Colors.red,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// appBar: PreferredSize(
|
||||
// preferredSize: const Size.fromHeight(
|
||||
// kToolbarHeight,
|
||||
// ),
|
||||
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
// builder: (context, state) {
|
||||
// if (state.selection.isEmpty) {
|
||||
// return DocumentSearchBar();
|
||||
// // return AppBar(
|
||||
// // title: Text(S.of(context).documentsPageTitle +
|
||||
// // " (${formatMaxCount(state.documents.length)})"),
|
||||
// // actions: [
|
||||
// // IconButton(
|
||||
// // icon: const Icon(Icons.search),
|
||||
// // onPressed: () {
|
||||
// // showMaterial3Search(
|
||||
// // context: context,
|
||||
// // delegate: DocumentSearchDelegate(
|
||||
// // DocumentSearchCubit(context.read()),
|
||||
// // searchFieldStyle:
|
||||
// // Theme.of(context).textTheme.bodyLarge,
|
||||
// // hintText: "Search documents", //TODO: INTL
|
||||
// // ),
|
||||
// // );
|
||||
// // },
|
||||
// // ),
|
||||
// // const SortDocumentsButton(),
|
||||
// // const AppOptionsPopupMenu(
|
||||
// // displayedActions: [
|
||||
// // AppPopupMenuEntries.documentsSelectListView,
|
||||
// // AppPopupMenuEntries.documentsSelectGridView,
|
||||
// // AppPopupMenuEntries.divider,
|
||||
// // AppPopupMenuEntries.openAboutThisAppDialog,
|
||||
// // AppPopupMenuEntries.reportBug,
|
||||
// // AppPopupMenuEntries.openSettings,
|
||||
// // ],
|
||||
// // ),
|
||||
// // ],
|
||||
// // );
|
||||
// } else {
|
||||
// return AppBar(
|
||||
// leading: IconButton(
|
||||
// icon: const Icon(Icons.close),
|
||||
// onPressed: () =>
|
||||
// context.read<DocumentsCubit>().resetSelection(),
|
||||
// ),
|
||||
// title: Text(
|
||||
// '${state.selection.length} ${S.of(context).documentsSelectedText}'),
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.delete),
|
||||
// onPressed: () => _onDelete(context, state),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
// builder: (context, state) {
|
||||
// final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
// return b.Badge(
|
||||
// position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||
// showBadge: appliedFiltersCount > 0,
|
||||
// badgeContent: Text(
|
||||
// '$appliedFiltersCount',
|
||||
// style: const TextStyle(
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// animationType: b.BadgeAnimationType.fade,
|
||||
// badgeColor: Colors.red,
|
||||
// child: FloatingActionButton(
|
||||
// child: const Icon(Icons.filter_alt_outlined),
|
||||
// onPressed: _openDocumentFilter,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
snap: true,
|
||||
title: SearchBar(
|
||||
height: kToolbarHeight - 2,
|
||||
supportingText: "Search documents",
|
||||
onTap: () {
|
||||
showDocumentSearchPage(context);
|
||||
},
|
||||
leadingIcon: Icon(Icons.menu),
|
||||
trailingIcon: CircleAvatar(
|
||||
child: Text("A"),
|
||||
),
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||
builder: (context, taskState) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildBody(connectivityState),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: _offset,
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return ColoredBox(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: SavedViewSelectionWidget(
|
||||
height: _savedViewWidgetHeight,
|
||||
currentFilter: state.filter,
|
||||
enabled: state.selection.isEmpty &&
|
||||
connectivityState.isConnected,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context
|
||||
.read<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||
builder: (context, taskState) {
|
||||
return _buildBody(connectivityState);
|
||||
// return Stack(
|
||||
// children: [
|
||||
// Positioned(
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// top: _offset,
|
||||
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
// builder: (context, state) {
|
||||
// return ColoredBox(
|
||||
// color:
|
||||
// Theme.of(context).colorScheme.background,
|
||||
// child: SavedViewSelectionWidget(
|
||||
// height: _savedViewWidgetHeight,
|
||||
// currentFilter: state.filter,
|
||||
// enabled: state.selection.isEmpty &&
|
||||
// connectivityState.isConnected,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -282,6 +293,28 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>
|
||||
_buildViewTypeButton() {
|
||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
builder: (context, settingsState) => IconButton(
|
||||
icon: Icon(
|
||||
settingsState.preferredViewType == ViewType.grid
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
),
|
||||
onPressed: () {
|
||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
||||
setState(() {
|
||||
_offset = 0;
|
||||
_last = 0;
|
||||
});
|
||||
final cubit = context.read<ApplicationSettingsCubit>();
|
||||
cubit.setViewType(cubit.state.preferredViewType.toggle());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -392,7 +425,26 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
||||
onStoragePathSelected: _addStoragePathToFilter,
|
||||
pageLoadingWidget: const NewItemsLoadingWidget(),
|
||||
beforeItems: const SizedBox(height: _savedViewWidgetHeight),
|
||||
beforeItems: SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SortDocumentsButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
settings.preferredViewType == ViewType.grid
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
),
|
||||
onPressed: () =>
|
||||
context.read<ApplicationSettingsCubit>().setViewType(
|
||||
settings.preferredViewType.toggle(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class DocumentsEmptyState extends StatelessWidget {
|
||||
final DocumentsPagedState state;
|
||||
final PagedDocumentsState state;
|
||||
final VoidCallback onReset;
|
||||
const DocumentsEmptyState({
|
||||
Key? key,
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
class AdaptiveDocumentsView extends StatelessWidget {
|
||||
final DocumentsState state;
|
||||
final ViewType viewType;
|
||||
final Widget beforeItems;
|
||||
final Widget? beforeItems;
|
||||
final void Function(DocumentModel) onTap;
|
||||
final void Function(DocumentModel) onSelected;
|
||||
final ScrollController scrollController;
|
||||
@@ -34,7 +34,7 @@ class AdaptiveDocumentsView extends StatelessWidget {
|
||||
this.onDocumentTypeSelected,
|
||||
this.onStoragePathSelected,
|
||||
required this.pageLoadingWidget,
|
||||
required this.beforeItems,
|
||||
this.beforeItems,
|
||||
required this.viewType,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,51 +4,60 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_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/translation/sort_field_localization_mapper.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/search/sort_field_selection_bottom_sheet.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
|
||||
class SortDocumentsButton extends StatelessWidget {
|
||||
const SortDocumentsButton({super.key});
|
||||
const SortDocumentsButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.sort),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
elevation: 2,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
||||
value: context.read<DocumentsCubit>(),
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<DocumentType>(
|
||||
context.read<
|
||||
LabelRepository<DocumentType,
|
||||
DocumentTypeRepositoryState>>(),
|
||||
),
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.filter.sortField == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return TextButton.icon(
|
||||
icon: Icon(state.filter.sortOrder == SortOrder.ascending
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward),
|
||||
label: Text(translateSortField(context, state.filter.sortField)),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
elevation: 2,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return SortFieldSelectionBottomSheet(
|
||||
),
|
||||
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
||||
value: context.read<DocumentsCubit>(),
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<DocumentType>(
|
||||
context.read<
|
||||
LabelRepository<DocumentType,
|
||||
DocumentTypeRepositoryState>>(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: SortFieldSelectionBottomSheet(
|
||||
initialSortField: state.filter.sortField,
|
||||
initialSortOrder: state.filter.sortOrder,
|
||||
onSubmit: (field, order) =>
|
||||
@@ -58,11 +67,11 @@ class SortDocumentsButton extends StatelessWidget {
|
||||
sortOrder: order,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -249,7 +249,7 @@ class _HomePageState extends State<HomePage> {
|
||||
builder: (context, sizingInformation) {
|
||||
if (!sizingInformation.isMobile) {
|
||||
return Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
// drawer: const AppDrawer(),
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
|
||||
@@ -317,53 +317,5 @@ class _AppDrawerState extends State<AppDrawer> {
|
||||
);
|
||||
}
|
||||
|
||||
Link _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.')
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onShowAboutDialog() {
|
||||
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 _onShowAboutDialog() {}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi
|
||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||
|
||||
class InboxCubit extends HydratedCubit<InboxState> with DocumentsPagingMixin {
|
||||
class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
|
||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||
_correspondentRepository;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
|
||||
part 'inbox_state.g.dart';
|
||||
|
||||
@JsonSerializable(
|
||||
ignoreUnannotated: true,
|
||||
)
|
||||
class InboxState extends DocumentsPagedState {
|
||||
class InboxState extends PagedDocumentsState {
|
||||
final Iterable<int> inboxTags;
|
||||
|
||||
final Map<int, Tag> availableTags;
|
||||
|
||||
@@ -72,10 +72,10 @@ class _InboxPageState extends State<InboxPage> {
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: Text(
|
||||
state.value.isEmpty
|
||||
? '0'
|
||||
: '${state.value.first.count} ' +
|
||||
S.of(context).inboxPageUnseenText,
|
||||
(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),
|
||||
|
||||
@@ -51,7 +51,6 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
[
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||
|
||||
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
|
||||
final PaperlessDocumentsApi _api;
|
||||
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState>
|
||||
with PagedDocumentsMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
LinkedDocumentsCubit(this._api, DocumentFilter filter)
|
||||
: super(LinkedDocumentsState(filter: filter)) {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
final documents = await _api.findAll(
|
||||
state.filter.copyWith(
|
||||
pageSize: 100,
|
||||
),
|
||||
);
|
||||
emit(LinkedDocumentsState(
|
||||
isLoaded: true,
|
||||
documents: documents,
|
||||
filter: state.filter,
|
||||
));
|
||||
LinkedDocumentsCubit(this.api, DocumentFilter filter)
|
||||
: super(const LinkedDocumentsState()) {
|
||||
updateFilter(filter: filter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
|
||||
class LinkedDocumentsState {
|
||||
final bool isLoaded;
|
||||
final PagedSearchResult<DocumentModel>? documents;
|
||||
final DocumentFilter filter;
|
||||
|
||||
LinkedDocumentsState({
|
||||
required this.filter,
|
||||
this.isLoaded = false,
|
||||
this.documents,
|
||||
class LinkedDocumentsState extends PagedDocumentsState {
|
||||
const LinkedDocumentsState({
|
||||
super.filter,
|
||||
super.isLoading,
|
||||
super.hasLoaded,
|
||||
super.value,
|
||||
});
|
||||
|
||||
LinkedDocumentsState copyWith({
|
||||
DocumentFilter? filter,
|
||||
bool? isLoading,
|
||||
bool? hasLoaded,
|
||||
List<PagedSearchResult<DocumentModel>>? value,
|
||||
}) {
|
||||
return LinkedDocumentsState(
|
||||
filter: filter ?? this.filter,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LinkedDocumentsState copyWithPaged({
|
||||
bool? hasLoaded,
|
||||
bool? isLoading,
|
||||
List<PagedSearchResult<DocumentModel>>? value,
|
||||
DocumentFilter? filter,
|
||||
}) {
|
||||
return copyWith(
|
||||
hasLoaded: hasLoaded,
|
||||
isLoading: isLoading,
|
||||
value: value,
|
||||
filter: filter,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
filter,
|
||||
isLoading,
|
||||
hasLoaded,
|
||||
value,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/list/document_l
|
||||
import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.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/helpers/message_helpers.dart';
|
||||
|
||||
class LinkedDocumentsPage extends StatefulWidget {
|
||||
const LinkedDocumentsPage({super.key});
|
||||
@@ -17,6 +18,28 @@ class LinkedDocumentsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_listenForLoadNewData);
|
||||
}
|
||||
|
||||
void _listenForLoadNewData() async {
|
||||
final currState = context.read<LinkedDocumentsCubit>().state;
|
||||
if (_scrollController.offset >=
|
||||
_scrollController.position.maxScrollExtent * 0.75 &&
|
||||
!currState.isLoading &&
|
||||
!currState.isLastPageLoaded) {
|
||||
try {
|
||||
await context.read<LinkedDocumentsCubit>().loadMore();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -25,45 +48,14 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
||||
),
|
||||
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).referencedDocumentsReadOnlyHintText,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
if (!state.isLoaded)
|
||||
const Expanded(child: DocumentsListLoadingWidget())
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: state.documents?.results.length,
|
||||
itemBuilder: (context, index) {
|
||||
return DocumentListItem(
|
||||
isLabelClickable: false,
|
||||
document: state.documents!.results.elementAt(index),
|
||||
onTap: (doc) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => DocumentDetailsCubit(
|
||||
context.read<PaperlessDocumentsApi>(),
|
||||
state.documents!.results.elementAt(index),
|
||||
),
|
||||
child: const DocumentDetailsPage(
|
||||
isLabelClickable: false,
|
||||
allowEdit: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!state.hasLoaded) {
|
||||
return const DocumentsListLoadingWidget();
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: state.documents.length,
|
||||
itemBuilder: (context, index) => DocumentListItem(
|
||||
document: state.documents[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,13 +5,13 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
/// Base state for all blocs/cubits using a paged view of documents.
|
||||
/// [T] is the return type of the API call.
|
||||
///
|
||||
abstract class DocumentsPagedState extends Equatable {
|
||||
abstract class PagedDocumentsState extends Equatable {
|
||||
final bool hasLoaded;
|
||||
final bool isLoading;
|
||||
final List<PagedSearchResult<DocumentModel>> value;
|
||||
final DocumentFilter filter;
|
||||
|
||||
const DocumentsPagedState({
|
||||
const PagedDocumentsState({
|
||||
this.value = const [],
|
||||
this.hasLoaded = false,
|
||||
this.isLoading = false,
|
||||
@@ -71,4 +71,12 @@ abstract class DocumentsPagedState extends Equatable {
|
||||
List<PagedSearchResult<DocumentModel>>? value,
|
||||
DocumentFilter? filter,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
filter,
|
||||
value,
|
||||
hasLoaded,
|
||||
isLoading,
|
||||
];
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
import 'model/documents_paged_state.dart';
|
||||
import 'model/paged_documents_state.dart';
|
||||
|
||||
///
|
||||
/// Mixin which can be used on cubits which handle documents. This implements all paging and filtering logic.
|
||||
///
|
||||
mixin DocumentsPagingMixin<State extends DocumentsPagedState>
|
||||
mixin PagedDocumentsMixin<State extends PagedDocumentsState>
|
||||
on BlocBase<State> {
|
||||
PaperlessDocumentsApi get api;
|
||||
|
||||
@@ -47,7 +47,6 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _openDocumentScanner(context),
|
||||
child: const Icon(Icons.add_a_photo_outlined),
|
||||
|
||||
68
lib/features/search/cubit/document_search_cubit.dart
Normal file
68
lib/features/search/cubit/document_search_cubit.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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>
|
||||
with PagedDocumentsMixin {
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
DocumentSearchCubit(this.api) : super(const DocumentSearchState());
|
||||
|
||||
Future<void> search(String query) async {
|
||||
emit(state.copyWith(
|
||||
isLoading: true,
|
||||
suggestions: [],
|
||||
view: SearchView.results,
|
||||
));
|
||||
final searchFilter = DocumentFilter(
|
||||
query: TextQuery.titleAndContent(query),
|
||||
);
|
||||
|
||||
await updateFilter(filter: searchFilter);
|
||||
emit(
|
||||
state.copyWith(
|
||||
searchHistory: [
|
||||
query,
|
||||
...state.searchHistory
|
||||
.whereNot((previousQuery) => previousQuery == query)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> suggest(String query) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: true,
|
||||
view: SearchView.suggestions,
|
||||
value: [],
|
||||
suggestions: [],
|
||||
),
|
||||
);
|
||||
final suggestions = await api.autocomplete(query);
|
||||
emit(state.copyWith(
|
||||
suggestions: suggestions,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void reset() {
|
||||
emit(state.copyWith(
|
||||
view: SearchView.suggestions,
|
||||
suggestions: [],
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
DocumentSearchState? fromJson(Map<String, dynamic> json) {
|
||||
return DocumentSearchState.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(DocumentSearchState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
|
||||
part 'document_search_state.g.dart';
|
||||
|
||||
enum SearchView {
|
||||
suggestions,
|
||||
results;
|
||||
}
|
||||
|
||||
@JsonSerializable(ignoreUnannotated: true)
|
||||
class DocumentSearchState extends DocumentsPagedState {
|
||||
class DocumentSearchState extends PagedDocumentsState {
|
||||
@JsonKey()
|
||||
final List<String> searchHistory;
|
||||
|
||||
final SearchView view;
|
||||
final List<String> suggestions;
|
||||
const DocumentSearchState({
|
||||
this.view = SearchView.suggestions,
|
||||
this.searchHistory = const [],
|
||||
this.suggestions = const [],
|
||||
super.filter,
|
||||
super.hasLoaded,
|
||||
super.isLoading,
|
||||
@@ -25,6 +33,8 @@ class DocumentSearchState extends DocumentsPagedState {
|
||||
filter,
|
||||
value,
|
||||
searchHistory,
|
||||
suggestions,
|
||||
view,
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -49,6 +59,7 @@ class DocumentSearchState extends DocumentsPagedState {
|
||||
List<PagedSearchResult<DocumentModel>>? value,
|
||||
DocumentFilter? filter,
|
||||
List<String>? suggestions,
|
||||
SearchView? view,
|
||||
}) {
|
||||
return DocumentSearchState(
|
||||
value: value ?? this.value,
|
||||
@@ -56,6 +67,8 @@ class DocumentSearchState extends DocumentsPagedState {
|
||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
searchHistory: searchHistory ?? this.searchHistory,
|
||||
view: view ?? this.view,
|
||||
suggestions: suggestions ?? this.suggestions,
|
||||
);
|
||||
}
|
||||
|
||||
166
lib/features/search/view/document_search_page.dart
Normal file
166
lib/features/search/view/document_search_page.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
||||
import 'package:paperless_mobile/features/search/cubit/document_search_state.dart';
|
||||
import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
Future<void> showDocumentSearchPage(BuildContext context) {
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => DocumentSearchCubit(context.read()),
|
||||
child: const DocumentSearchPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class DocumentSearchPage extends StatefulWidget {
|
||||
const DocumentSearchPage({super.key});
|
||||
|
||||
@override
|
||||
State<DocumentSearchPage> createState() => _DocumentSearchPageState();
|
||||
}
|
||||
|
||||
class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
||||
final _queryController = TextEditingController(text: '');
|
||||
|
||||
String get query => _queryController.text;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
toolbarHeight: 72,
|
||||
leading: BackButton(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
style: theme.textTheme.bodyLarge?.apply(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
hintText: "Search documents",
|
||||
border: InputBorder.none,
|
||||
),
|
||||
controller: _queryController,
|
||||
onChanged: context.read<DocumentSearchCubit>().suggest,
|
||||
onSubmitted: context.read<DocumentSearchCubit>().search,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
context.read<DocumentSearchCubit>().reset();
|
||||
_queryController.clear();
|
||||
},
|
||||
)
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
|
||||
builder: (context, state) {
|
||||
switch (state.view) {
|
||||
case SearchView.suggestions:
|
||||
return _buildSuggestionsView(state);
|
||||
case SearchView.results:
|
||||
return _buildResultsView(state);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestionsView(DocumentSearchState state) {
|
||||
final suggestions = state.suggestions
|
||||
.whereNot((element) => state.searchHistory.contains(element))
|
||||
.toList();
|
||||
final historyMatches = state.searchHistory
|
||||
.where(
|
||||
(element) => element.startsWith(query),
|
||||
)
|
||||
.toList();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => ListTile(
|
||||
title: Text(historyMatches[index]),
|
||||
leading: Icon(Icons.history),
|
||||
onTap: () => _selectSuggestion(historyMatches[index]),
|
||||
),
|
||||
childCount: historyMatches.length,
|
||||
),
|
||||
),
|
||||
if (state.isLoading)
|
||||
const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => ListTile(
|
||||
title: Text(suggestions[index]),
|
||||
leading: Icon(Icons.search),
|
||||
onTap: () => _selectSuggestion(suggestions[index]),
|
||||
),
|
||||
childCount: suggestions.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultsView(DocumentSearchState state) {
|
||||
final header = Text(
|
||||
S.of(context).documentSearchResults,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
).padded();
|
||||
if (state.isLoading) {
|
||||
return DocumentsListLoadingWidget(
|
||||
beforeWidgets: [header],
|
||||
);
|
||||
}
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: header),
|
||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(child: Text("No documents found.")),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => DocumentListItem(
|
||||
document: state.documents[index],
|
||||
),
|
||||
childCount: state.documents.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _selectSuggestion(String suggestion) {
|
||||
context.read<DocumentSearchCubit>().search(suggestion);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
part 'application_settings_state.g.dart';
|
||||
|
||||
@@ -13,7 +14,7 @@ part 'application_settings_state.g.dart';
|
||||
@JsonSerializable()
|
||||
class ApplicationSettingsState {
|
||||
static final defaultSettings = ApplicationSettingsState(
|
||||
preferredLocaleSubtag: Platform.localeName.split('_').first,
|
||||
preferredLocaleSubtag: _defaultPreferredLocaleSubtag,
|
||||
);
|
||||
|
||||
final bool isLocalAuthenticationEnabled;
|
||||
@@ -52,4 +53,13 @@ class ApplicationSettingsState {
|
||||
preferredColorSchemeOption ?? this.preferredColorSchemeOption,
|
||||
);
|
||||
}
|
||||
|
||||
static String get _defaultPreferredLocaleSubtag {
|
||||
String preferredLocale = Platform.localeName.split("_").first;
|
||||
if (!S.delegate.supportedLocales
|
||||
.any((locale) => locale.languageCode == preferredLocale)) {
|
||||
preferredLocale = 'en';
|
||||
}
|
||||
return preferredLocale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
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/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/pages/storage_settings_page.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
@@ -14,22 +26,58 @@ 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<PaperlessServerInformationCubit,
|
||||
PaperlessServerInformationState>(
|
||||
builder: (context, state) {
|
||||
final info = state.information!;
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
S.of(context).appDrawerHeaderLoggedInAsText +
|
||||
" " +
|
||||
(info.username ?? 'unknown') +
|
||||
"@${info.host}",
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
subtitle: Text(
|
||||
S.of(context).serverInformationPaperlessVersionText +
|
||||
' ' +
|
||||
info.version.toString() +
|
||||
' (API v${info.apiVersion})',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
// leading: const Icon(Icons.style_outlined),
|
||||
title: Text(S.of(context).settingsPageApplicationSettingsLabel),
|
||||
subtitle: Text(
|
||||
S.of(context).settingsPageApplicationSettingsDescriptionText),
|
||||
onTap: () => _goto(const ApplicationSettingsPage(), context),
|
||||
),
|
||||
ListTile(
|
||||
// leading: const Icon(Icons.security_outlined),
|
||||
title: Text(S.of(context).settingsPageSecuritySettingsLabel),
|
||||
subtitle:
|
||||
Text(S.of(context).settingsPageSecuritySettingsDescriptionText),
|
||||
onTap: () => _goto(const SecuritySettingsPage(), context),
|
||||
),
|
||||
ListTile(
|
||||
// leading: const Icon(Icons.storage_outlined),
|
||||
title: Text(S.of(context).settingsPageStorageSettingsLabel),
|
||||
subtitle:
|
||||
Text(S.of(context).settingsPageStorageSettingsDescriptionText),
|
||||
@@ -52,4 +100,25 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.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/view/widgets/radio_settings_dialog.dart';
|
||||
@@ -20,6 +21,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
'cs': 'Česky',
|
||||
'tr': 'Türkçe',
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
@@ -27,9 +29,12 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
return ListTile(
|
||||
title: Text(S.of(context).settingsPageLanguageSettingLabel),
|
||||
subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!),
|
||||
onTap: () => showDialog(
|
||||
onTap: () => showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => RadioSettingsDialog<String>(
|
||||
footer: const Text(
|
||||
"* Work in progress, not fully translated yet. Some words may be displayed in English!",
|
||||
),
|
||||
titleText: S.of(context).settingsPageLanguageSettingLabel,
|
||||
options: [
|
||||
RadioOption(
|
||||
@@ -42,11 +47,11 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
),
|
||||
RadioOption(
|
||||
value: 'cs',
|
||||
label: _languageOptions['cs']!,
|
||||
label: _languageOptions['cs']! + " *",
|
||||
),
|
||||
RadioOption(
|
||||
value: 'tr',
|
||||
label: _languageOptions['tr']!,
|
||||
label: _languageOptions['tr']! + " *",
|
||||
)
|
||||
],
|
||||
initialValue: context
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
|
||||
part 'similar_documents_state.dart';
|
||||
|
||||
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||
with DocumentsPagingMixin<SimilarDocumentsState> {
|
||||
with PagedDocumentsMixin<SimilarDocumentsState> {
|
||||
final int documentId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
part of 'similar_documents_cubit.dart';
|
||||
|
||||
class SimilarDocumentsState extends DocumentsPagedState {
|
||||
class SimilarDocumentsState extends PagedDocumentsState {
|
||||
const SimilarDocumentsState({
|
||||
super.filter,
|
||||
super.hasLoaded,
|
||||
|
||||
@@ -4,7 +4,7 @@ String formatMaxCount(int? count, [int maxCount = 99]) {
|
||||
if ((count ?? 0) > maxCount) {
|
||||
return "$maxCount+";
|
||||
}
|
||||
return (count ?? 0).toString().padLeft(maxCount.toString().length);
|
||||
return (count ?? 0).toString();
|
||||
}
|
||||
|
||||
String formatBytes(int bytes, int decimals) {
|
||||
|
||||
Reference in New Issue
Block a user