mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 12:07:54 -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_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
String translateSortField(BuildContext context, SortField sortField) {
|
String translateSortField(BuildContext context, SortField? sortField) {
|
||||||
switch (sortField) {
|
switch (sortField) {
|
||||||
case SortField.archiveSerialNumber:
|
case SortField.archiveSerialNumber:
|
||||||
return S.of(context).documentArchiveSerialNumberPropertyShortLabel;
|
return S.of(context).documentArchiveSerialNumberPropertyShortLabel;
|
||||||
@@ -18,5 +18,7 @@ String translateSortField(BuildContext context, SortField sortField) {
|
|||||||
return S.of(context).documentAddedPropertyLabel;
|
return S.of(context).documentAddedPropertyLabel;
|
||||||
case SortField.modified:
|
case SortField.modified:
|
||||||
return S.of(context).documentModifiedPropertyLabel;
|
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,14 +28,12 @@ class SearchBar extends StatelessWidget {
|
|||||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(minWidth: 360, maxWidth: 720),
|
constraints: const BoxConstraints(minWidth: 360, maxWidth: 720),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: effectiveHeight,
|
height: effectiveHeight,
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 3,
|
elevation: 1,
|
||||||
color: colorScheme.surface,
|
color: colorScheme.surface,
|
||||||
shadowColor: colorScheme.shadow,
|
shadowColor: colorScheme.shadow,
|
||||||
surfaceTintColor: colorScheme.surfaceTint,
|
surfaceTintColor: colorScheme.surfaceTint,
|
||||||
@@ -53,14 +51,14 @@ class SearchBar extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
readOnly: true,
|
||||||
cursorColor: colorScheme.primary,
|
cursorColor: colorScheme.primary,
|
||||||
style: textTheme.bodyLarge,
|
style: textTheme.bodyLarge,
|
||||||
textAlignVertical: TextAlignVertical.center,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
hintText: supportingText,
|
hintText: supportingText,
|
||||||
hintStyle: textTheme.bodyLarge?.apply(
|
hintStyle: textTheme.bodyLarge?.apply(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
@@ -75,7 +73,6 @@ class SearchBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.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/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>
|
class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||||
with DocumentsPagingMixin {
|
with PagedDocumentsMixin {
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.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;
|
final int? selectedSavedViewId;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ 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/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/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/bloc/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.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_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/login/bloc/authentication_cubit.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/saved_view_selection_widget.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_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';
|
||||||
@@ -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/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
|
||||||
|
|
||||||
class DocumentFilterIntent {
|
class DocumentFilterIntent {
|
||||||
final DocumentFilter? filter;
|
final DocumentFilter? filter;
|
||||||
@@ -137,43 +139,163 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
const linearProgressIndicatorHeight = 4.0;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: BlocProvider.value(
|
// appBar: PreferredSize(
|
||||||
value: context.read<AuthenticationCubit>(),
|
// preferredSize: const Size.fromHeight(
|
||||||
child: AppDrawer(
|
// kToolbarHeight,
|
||||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
// ),
|
||||||
|
// 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: 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"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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>(
|
|
||||||
|
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>
|
||||||
|
_buildViewTypeButton() {
|
||||||
|
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
builder: (context, settingsState) => IconButton(
|
builder: (context, settingsState) => IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
settingsState.preferredViewType == ViewType.grid
|
settingsState.preferredViewType == ViewType.grid
|
||||||
@@ -186,97 +308,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
_offset = 0;
|
_offset = 0;
|
||||||
_last = 0;
|
_last = 0;
|
||||||
});
|
});
|
||||||
final cubit =
|
final cubit = context.read<ApplicationSettingsCubit>();
|
||||||
context.read<ApplicationSettingsCubit>();
|
cubit.setViewType(cubit.state.preferredViewType.toggle());
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
resizeToAvoidBottomInset: true,
|
|
||||||
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 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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -392,7 +425,26 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
||||||
onStoragePathSelected: _addStoragePathToFilter,
|
onStoragePathSelected: _addStoragePathToFilter,
|
||||||
pageLoadingWidget: const NewItemsLoadingWidget(),
|
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_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
class DocumentsEmptyState extends StatelessWidget {
|
class DocumentsEmptyState extends StatelessWidget {
|
||||||
final DocumentsPagedState state;
|
final PagedDocumentsState state;
|
||||||
final VoidCallback onReset;
|
final VoidCallback onReset;
|
||||||
const DocumentsEmptyState({
|
const DocumentsEmptyState({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|||||||
class AdaptiveDocumentsView extends StatelessWidget {
|
class AdaptiveDocumentsView extends StatelessWidget {
|
||||||
final DocumentsState state;
|
final DocumentsState state;
|
||||||
final ViewType viewType;
|
final ViewType viewType;
|
||||||
final Widget beforeItems;
|
final Widget? beforeItems;
|
||||||
final void Function(DocumentModel) onTap;
|
final void Function(DocumentModel) onTap;
|
||||||
final void Function(DocumentModel) onSelected;
|
final void Function(DocumentModel) onSelected;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
@@ -34,7 +34,7 @@ class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
this.onDocumentTypeSelected,
|
this.onDocumentTypeSelected,
|
||||||
this.onStoragePathSelected,
|
this.onStoragePathSelected,
|
||||||
required this.pageLoadingWidget,
|
required this.pageLoadingWidget,
|
||||||
required this.beforeItems,
|
this.beforeItems,
|
||||||
required this.viewType,
|
required this.viewType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,29 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/repository/label_repository.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/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/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_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/search/sort_field_selection_bottom_sheet.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';
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
|
|
||||||
class SortDocumentsButton extends StatelessWidget {
|
class SortDocumentsButton extends StatelessWidget {
|
||||||
const SortDocumentsButton({super.key});
|
const SortDocumentsButton({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
icon: const Icon(Icons.sort),
|
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: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@@ -46,9 +57,7 @@ class SortDocumentsButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
child: SortFieldSelectionBottomSheet(
|
||||||
builder: (context, state) {
|
|
||||||
return SortFieldSelectionBottomSheet(
|
|
||||||
initialSortField: state.filter.sortField,
|
initialSortField: state.filter.sortField,
|
||||||
initialSortOrder: state.filter.sortOrder,
|
initialSortOrder: state.filter.sortOrder,
|
||||||
onSubmit: (field, order) =>
|
onSubmit: (field, order) =>
|
||||||
@@ -58,11 +67,11 @@ class SortDocumentsButton extends StatelessWidget {
|
|||||||
sortOrder: order,
|
sortOrder: order,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
builder: (context, sizingInformation) {
|
builder: (context, sizingInformation) {
|
||||||
if (!sizingInformation.isMobile) {
|
if (!sizingInformation.isMobile) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const AppDrawer(),
|
// drawer: const AppDrawer(),
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
NavigationRail(
|
||||||
|
|||||||
@@ -317,53 +317,5 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Link _buildOnboardingImageCredits() {
|
void _onShowAboutDialog() {}
|
||||||
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(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/document_type_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/features/inbox/bloc/state/inbox_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<Tag, TagRepositoryState> _tagsRepository;
|
||||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||||
_correspondentRepository;
|
_correspondentRepository;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.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';
|
part 'inbox_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable(
|
@JsonSerializable(
|
||||||
ignoreUnannotated: true,
|
ignoreUnannotated: true,
|
||||||
)
|
)
|
||||||
class InboxState extends DocumentsPagedState {
|
class InboxState extends PagedDocumentsState {
|
||||||
final Iterable<int> inboxTags;
|
final Iterable<int> inboxTags;
|
||||||
|
|
||||||
final Map<int, Tag> availableTags;
|
final Map<int, Tag> availableTags;
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
child: Text(
|
child: Text(
|
||||||
state.value.isEmpty
|
(state.value.isEmpty
|
||||||
? '0'
|
? '0 '
|
||||||
: '${state.value.first.count} ' +
|
: '${state.value.first.count} ') +
|
||||||
S.of(context).inboxPageUnseenText,
|
S.of(context).inboxPageUnseenText,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectedState) {
|
builder: (context, connectedState) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const AppDrawer(),
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
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/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/features/paged_document_view/paged_documents_mixin.dart';
|
||||||
|
|
||||||
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
|
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState>
|
||||||
final PaperlessDocumentsApi _api;
|
with PagedDocumentsMixin {
|
||||||
|
@override
|
||||||
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
LinkedDocumentsCubit(this._api, DocumentFilter filter)
|
LinkedDocumentsCubit(this.api, DocumentFilter filter)
|
||||||
: super(LinkedDocumentsState(filter: filter)) {
|
: super(const LinkedDocumentsState()) {
|
||||||
_initialize();
|
updateFilter(filter: filter);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
|
||||||
final documents = await _api.findAll(
|
|
||||||
state.filter.copyWith(
|
|
||||||
pageSize: 100,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
emit(LinkedDocumentsState(
|
|
||||||
isLoaded: true,
|
|
||||||
documents: documents,
|
|
||||||
filter: state.filter,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,48 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||||
|
|
||||||
class LinkedDocumentsState {
|
class LinkedDocumentsState extends PagedDocumentsState {
|
||||||
final bool isLoaded;
|
const LinkedDocumentsState({
|
||||||
final PagedSearchResult<DocumentModel>? documents;
|
super.filter,
|
||||||
final DocumentFilter filter;
|
super.isLoading,
|
||||||
|
super.hasLoaded,
|
||||||
LinkedDocumentsState({
|
super.value,
|
||||||
required this.filter,
|
|
||||||
this.isLoaded = false,
|
|
||||||
this.documents,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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/linked_documents_cubit.dart';
|
||||||
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';
|
||||||
|
|
||||||
class LinkedDocumentsPage extends StatefulWidget {
|
class LinkedDocumentsPage extends StatefulWidget {
|
||||||
const LinkedDocumentsPage({super.key});
|
const LinkedDocumentsPage({super.key});
|
||||||
@@ -17,6 +18,28 @@ class LinkedDocumentsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -25,45 +48,14 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
),
|
),
|
||||||
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
if (!state.hasLoaded) {
|
||||||
children: [
|
return const DocumentsListLoadingWidget();
|
||||||
Text(
|
}
|
||||||
S.of(context).referencedDocumentsReadOnlyHintText,
|
return ListView.builder(
|
||||||
textAlign: TextAlign.center,
|
itemCount: state.documents.length,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
itemBuilder: (context, index) => DocumentListItem(
|
||||||
|
document: state.documents[index],
|
||||||
),
|
),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
/// Base state for all blocs/cubits using a paged view of documents.
|
/// Base state for all blocs/cubits using a paged view of documents.
|
||||||
/// [T] is the return type of the API call.
|
/// [T] is the return type of the API call.
|
||||||
///
|
///
|
||||||
abstract class DocumentsPagedState extends Equatable {
|
abstract class PagedDocumentsState extends Equatable {
|
||||||
final bool hasLoaded;
|
final bool hasLoaded;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final List<PagedSearchResult<DocumentModel>> value;
|
final List<PagedSearchResult<DocumentModel>> value;
|
||||||
final DocumentFilter filter;
|
final DocumentFilter filter;
|
||||||
|
|
||||||
const DocumentsPagedState({
|
const PagedDocumentsState({
|
||||||
this.value = const [],
|
this.value = const [],
|
||||||
this.hasLoaded = false,
|
this.hasLoaded = false,
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
@@ -71,4 +71,12 @@ abstract class DocumentsPagedState extends Equatable {
|
|||||||
List<PagedSearchResult<DocumentModel>>? value,
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
DocumentFilter? filter,
|
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.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 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> {
|
on BlocBase<State> {
|
||||||
PaperlessDocumentsApi get api;
|
PaperlessDocumentsApi get api;
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ 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),
|
||||||
|
|||||||
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:equatable/equatable.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.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';
|
part 'document_search_state.g.dart';
|
||||||
|
|
||||||
|
enum SearchView {
|
||||||
|
suggestions,
|
||||||
|
results;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonSerializable(ignoreUnannotated: true)
|
@JsonSerializable(ignoreUnannotated: true)
|
||||||
class DocumentSearchState extends DocumentsPagedState {
|
class DocumentSearchState extends PagedDocumentsState {
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final List<String> searchHistory;
|
final List<String> searchHistory;
|
||||||
|
final SearchView view;
|
||||||
|
final List<String> suggestions;
|
||||||
const DocumentSearchState({
|
const DocumentSearchState({
|
||||||
|
this.view = SearchView.suggestions,
|
||||||
this.searchHistory = const [],
|
this.searchHistory = const [],
|
||||||
|
this.suggestions = const [],
|
||||||
super.filter,
|
super.filter,
|
||||||
super.hasLoaded,
|
super.hasLoaded,
|
||||||
super.isLoading,
|
super.isLoading,
|
||||||
@@ -25,6 +33,8 @@ class DocumentSearchState extends DocumentsPagedState {
|
|||||||
filter,
|
filter,
|
||||||
value,
|
value,
|
||||||
searchHistory,
|
searchHistory,
|
||||||
|
suggestions,
|
||||||
|
view,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,6 +59,7 @@ class DocumentSearchState extends DocumentsPagedState {
|
|||||||
List<PagedSearchResult<DocumentModel>>? value,
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
DocumentFilter? filter,
|
DocumentFilter? filter,
|
||||||
List<String>? suggestions,
|
List<String>? suggestions,
|
||||||
|
SearchView? view,
|
||||||
}) {
|
}) {
|
||||||
return DocumentSearchState(
|
return DocumentSearchState(
|
||||||
value: value ?? this.value,
|
value: value ?? this.value,
|
||||||
@@ -56,6 +67,8 @@ class DocumentSearchState extends DocumentsPagedState {
|
|||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
searchHistory: searchHistory ?? this.searchHistory,
|
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:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.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/features/settings/model/view_type.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
part 'application_settings_state.g.dart';
|
part 'application_settings_state.g.dart';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ part 'application_settings_state.g.dart';
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ApplicationSettingsState {
|
class ApplicationSettingsState {
|
||||||
static final defaultSettings = ApplicationSettingsState(
|
static final defaultSettings = ApplicationSettingsState(
|
||||||
preferredLocaleSubtag: Platform.localeName.split('_').first,
|
preferredLocaleSubtag: _defaultPreferredLocaleSubtag,
|
||||||
);
|
);
|
||||||
|
|
||||||
final bool isLocalAuthenticationEnabled;
|
final bool isLocalAuthenticationEnabled;
|
||||||
@@ -52,4 +53,13 @@ class ApplicationSettingsState {
|
|||||||
preferredColorSchemeOption ?? this.preferredColorSchemeOption,
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/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/application_settings_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/pages/security_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/features/settings/view/pages/storage_settings_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
@@ -14,22 +26,58 @@ 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,
|
||||||
|
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(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
|
// leading: const Icon(Icons.style_outlined),
|
||||||
title: Text(S.of(context).settingsPageApplicationSettingsLabel),
|
title: Text(S.of(context).settingsPageApplicationSettingsLabel),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
S.of(context).settingsPageApplicationSettingsDescriptionText),
|
S.of(context).settingsPageApplicationSettingsDescriptionText),
|
||||||
onTap: () => _goto(const ApplicationSettingsPage(), context),
|
onTap: () => _goto(const ApplicationSettingsPage(), context),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
// leading: const Icon(Icons.security_outlined),
|
||||||
title: Text(S.of(context).settingsPageSecuritySettingsLabel),
|
title: Text(S.of(context).settingsPageSecuritySettingsLabel),
|
||||||
subtitle:
|
subtitle:
|
||||||
Text(S.of(context).settingsPageSecuritySettingsDescriptionText),
|
Text(S.of(context).settingsPageSecuritySettingsDescriptionText),
|
||||||
onTap: () => _goto(const SecuritySettingsPage(), context),
|
onTap: () => _goto(const SecuritySettingsPage(), context),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
// leading: const Icon(Icons.storage_outlined),
|
||||||
title: Text(S.of(context).settingsPageStorageSettingsLabel),
|
title: Text(S.of(context).settingsPageStorageSettingsLabel),
|
||||||
subtitle:
|
subtitle:
|
||||||
Text(S.of(context).settingsPageStorageSettingsDescriptionText),
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_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/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
@@ -20,6 +21,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
'cs': 'Česky',
|
'cs': 'Česky',
|
||||||
'tr': 'Türkçe',
|
'tr': 'Türkçe',
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
@@ -27,9 +29,12 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(S.of(context).settingsPageLanguageSettingLabel),
|
title: Text(S.of(context).settingsPageLanguageSettingLabel),
|
||||||
subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!),
|
subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!),
|
||||||
onTap: () => showDialog(
|
onTap: () => showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => RadioSettingsDialog<String>(
|
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,
|
titleText: S.of(context).settingsPageLanguageSettingLabel,
|
||||||
options: [
|
options: [
|
||||||
RadioOption(
|
RadioOption(
|
||||||
@@ -42,11 +47,11 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
),
|
),
|
||||||
RadioOption(
|
RadioOption(
|
||||||
value: 'cs',
|
value: 'cs',
|
||||||
label: _languageOptions['cs']!,
|
label: _languageOptions['cs']! + " *",
|
||||||
),
|
),
|
||||||
RadioOption(
|
RadioOption(
|
||||||
value: 'tr',
|
value: 'tr',
|
||||||
label: _languageOptions['tr']!,
|
label: _languageOptions['tr']! + " *",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
initialValue: context
|
initialValue: context
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.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/paged_documents_mixin.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 'similar_documents_state.dart';
|
part 'similar_documents_state.dart';
|
||||||
|
|
||||||
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||||
with DocumentsPagingMixin<SimilarDocumentsState> {
|
with PagedDocumentsMixin<SimilarDocumentsState> {
|
||||||
final int documentId;
|
final int documentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
part of 'similar_documents_cubit.dart';
|
part of 'similar_documents_cubit.dart';
|
||||||
|
|
||||||
class SimilarDocumentsState extends DocumentsPagedState {
|
class SimilarDocumentsState extends PagedDocumentsState {
|
||||||
const SimilarDocumentsState({
|
const SimilarDocumentsState({
|
||||||
super.filter,
|
super.filter,
|
||||||
super.hasLoaded,
|
super.hasLoaded,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ String formatMaxCount(int? count, [int maxCount = 99]) {
|
|||||||
if ((count ?? 0) > maxCount) {
|
if ((count ?? 0) > maxCount) {
|
||||||
return "$maxCount+";
|
return "$maxCount+";
|
||||||
}
|
}
|
||||||
return (count ?? 0).toString().padLeft(maxCount.toString().length);
|
return (count ?? 0).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatBytes(int bytes, int decimals) {
|
String formatBytes(int bytes, int decimals) {
|
||||||
|
|||||||
Reference in New Issue
Block a user