Reworked inbox, added more information and allow suggestions to be directly clicked

This commit is contained in:
Anton Stubenbord
2023-01-15 01:05:57 +01:00
parent 21462c0463
commit 31b6335f95
13 changed files with 551 additions and 210 deletions

View File

@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
@@ -22,12 +23,15 @@ import 'package:paperless_mobile/features/labels/correspondent/view/widgets/corr
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:badges/badges.dart' as b;
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
class DocumentDetailsPage extends StatefulWidget {
final bool allowEdit;
final bool isLabelClickable;
@@ -284,7 +288,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
content: document.archiveSerialNumber != null
? Text(document.archiveSerialNumber.toString())
: TextButton.icon(
icon: const Icon(Icons.archive),
icon: const Icon(Icons.archive_outlined),
label: Text(S
.of(context)
.documentDetailsPageAssignAsnButtonLabel),
@@ -369,37 +373,35 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return ListView(
children: [
_DetailsItem(
label: S.of(context).documentTitlePropertyLabel,
content: HighlightedText(
text: document.title,
highlights: widget.titleAndContentQueryString?.split(" ") ?? [],
style: Theme.of(context).textTheme.bodyLarge,
),
label: S.of(context).documentTitlePropertyLabel,
).paddedOnly(bottom: 16),
_DetailsItem.text(
DateFormat.yMMMd().format(document.created),
DateFormat.yMMMMd().format(document.created),
context: context,
label: S.of(context).documentCreatedPropertyLabel,
).paddedSymmetrically(vertical: 16),
Visibility(
visible: document.documentType != null,
child: _DetailsItem(
content: DocumentTypeWidget(
textStyle: Theme.of(context).textTheme.bodyLarge,
isClickable: widget.isLabelClickable,
documentTypeId: document.documentType,
),
label: S.of(context).documentDocumentTypePropertyLabel,
content: LabelText<DocumentType, DocumentTypeRepositoryState>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.documentType,
),
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.correspondent != null,
child: _DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel,
content: CorrespondentWidget(
textStyle: Theme.of(context).textTheme.bodyLarge,
isClickable: widget.isLabelClickable,
correspondentId: document.correspondent,
content: LabelText<Correspondent, CorrespondentRepositoryState>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.correspondent,
),
).paddedSymmetrically(vertical: 16),
),
@@ -521,8 +523,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
class _DetailsItem extends StatelessWidget {
final String label;
final Widget content;
const _DetailsItem({Key? key, required this.label, required this.content})
: super(key: key);
const _DetailsItem({
Key? key,
required this.label,
required this.content,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -545,7 +550,10 @@ class _DetailsItem extends StatelessWidget {
String text, {
required this.label,
required BuildContext context,
}) : content = Text(text, style: Theme.of(context).textTheme.bodyLarge);
}) : content = Text(
text,
style: Theme.of(context).textTheme.bodyLarge,
);
}
class ColoredTabBar extends Container implements PreferredSizeWidget {

View File

@@ -19,6 +19,7 @@ import 'package:paperless_mobile/features/document_upload/cubit/document_upload_
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/route_description.dart';
import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
@@ -145,6 +146,71 @@ class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
final destinations = [
RouteDescription(
icon: const Icon(Icons.description_outlined),
selectedIcon: Icon(
Icons.description,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context).bottomNavDocumentsPageLabel,
),
RouteDescription(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context).bottomNavScannerPageLabel,
),
RouteDescription(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
Icons.sell,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context).bottomNavLabelsPageLabel,
),
// RouteDescription(
// icon: const Icon(Icons.inbox_outlined),
// selectedIcon: Icon(
// Icons.inbox,
// color: Theme.of(context).colorScheme.primary,
// ),
// label: S.of(context).bottomNavInboxPageLabel,
// ),
// RouteDescription(
// icon: const Icon(Icons.settings_outlined),
// selectedIcon: Icon(
// Icons.settings,
// color: Theme.of(context).colorScheme.primary,
// ),
// label: S.of(context).appDrawerSettingsLabel,
// ),
];
final routes = <Widget>[
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
),
),
BlocProvider(
create: (context) => SavedViewCubit(
context.read<SavedViewRepository>(),
),
),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
),
const LabelsPage(),
];
return MultiBlocListener(
listeners: [
BlocListener<ConnectivityCubit, ConnectivityState>(
@@ -172,96 +238,35 @@ class _HomePageState extends State<HomePage> {
return Scaffold(
key: rootScaffoldKey,
drawer: const InfoDrawer(),
body: Row(children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: [
NavigationRailDestination(
icon: const Icon(Icons.description_outlined),
selectedIcon: Icon(
Icons.description,
color: Theme.of(context).colorScheme.primary,
),
label: Text(S.of(context).bottomNavDocumentsPageLabel),
),
NavigationRailDestination(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: Theme.of(context).colorScheme.primary,
),
label: Text(S.of(context).bottomNavScannerPageLabel),
),
NavigationRailDestination(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
Icons.sell,
color: Theme.of(context).colorScheme.primary,
),
label: Text(S.of(context).bottomNavLabelsPageLabel),
),
],
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: [
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
),
),
BlocProvider(
create: (context) => SavedViewCubit(
context.read<SavedViewRepository>(),
),
),
],
child: const DocumentsPage(),
body: Row(
children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: destinations
.map((e) => e.toNavigationRailDestination())
.toList(),
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: routes[_currentIndex],
),
const LabelsPage(),
][_currentIndex]),
]),
],
),
);
}
return Scaffold(
key: rootScaffoldKey,
bottomNavigationBar: BottomNavBar(
bottomNavigationBar: NavigationBar(
elevation: 4.0,
selectedIndex: _currentIndex,
onNavigationChanged: _onNavigationChanged,
onDestinationSelected: _onNavigationChanged,
destinations:
destinations.map((e) => e.toNavigationDestination()).toList(),
),
drawer: const InfoDrawer(),
body: [
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read<PaperlessDocumentsApi>(),
context.read<SavedViewRepository>(),
),
),
BlocProvider(
create: (context) => SavedViewCubit(
context.read<SavedViewRepository>(),
),
),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: _scannerCubit,
child: const ScannerPage(),
),
const LabelsPage(),
][_currentIndex],
body: routes[_currentIndex],
);
},
),

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class RouteDescription {
final String label;
final Icon icon;
final Icon selectedIcon;
RouteDescription({
required this.label,
required this.icon,
required this.selectedIcon,
});
NavigationDestination toNavigationDestination() {
return NavigationDestination(
label: label,
icon: icon,
selectedIcon: selectedIcon,
);
}
NavigationRailDestination toNavigationRailDestination() {
return NavigationRailDestination(
label: Text(label),
icon: icon,
selectedIcon: selectedIcon,
);
}
BottomNavigationBarItem toBottomNavigationBarItem() {
return BottomNavigationBarItem(
label: label,
icon: icon,
activeIcon: selectedIcon,
);
}
}

View File

@@ -42,6 +42,22 @@ class BottomNavBar extends StatelessWidget {
),
label: S.of(context).bottomNavLabelsPageLabel,
),
NavigationDestination(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: Icon(
Icons.inbox,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context).bottomNavInboxPageLabel,
),
NavigationDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: Icon(
Icons.settings,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context).appDrawerSettingsLabel,
),
],
);
}

View File

@@ -307,8 +307,10 @@ class _InfoDrawerState extends State<InfoDrawer> {
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => InboxCubit(
context.read<LabelRepository<Tag, TagRepositoryState>>(),
context.read<PaperlessDocumentsApi>(),
context.read(),
context.read(),
context.read(),
context.read(),
)..initializeInbox(),
child: const InboxPage(),
),

View File

@@ -1,16 +1,64 @@
import 'dart:async';
import 'package:collection/collection.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/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/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
class InboxCubit extends HydratedCubit<InboxState> {
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
final LabelRepository<Correspondent, CorrespondentRepositoryState>
_correspondentRepository;
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
_documentTypeRepository;
final PaperlessDocumentsApi _documentsApi;
InboxCubit(this._tagsRepository, this._documentsApi)
: super(const InboxState());
final List<StreamSubscription> _subscriptions = [];
InboxCubit(
this._tagsRepository,
this._documentsApi,
this._correspondentRepository,
this._documentTypeRepository,
) : super(
InboxState(
availableCorrespondents:
_correspondentRepository.current?.values ?? {},
availableDocumentTypes:
_documentTypeRepository.current?.values ?? {},
availableTags: _tagsRepository.current?.values ?? {},
),
) {
_subscriptions.add(
_tagsRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(availableTags: event!.values));
}
}),
);
_subscriptions.add(
_correspondentRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(
availableCorrespondents: event!.values,
));
}
}),
);
_subscriptions.add(
_documentTypeRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(availableDocumentTypes: event!.values));
}
}),
);
}
///
/// Fetches inbox tag ids and loads the inbox items (documents).
@@ -135,6 +183,39 @@ class InboxCubit extends HydratedCubit<InboxState> {
}
}
Future<void> updateDocument(DocumentModel document) async {
final updatedDocument = await _documentsApi.update(document);
emit(
state.copyWith(
inboxItems: state.inboxItems.map(
(e) => e.id == document.id ? updatedDocument : e,
),
),
);
}
Future<void> deleteDocument(DocumentModel document) async {
int deletedId = await _documentsApi.delete(document);
emit(
state.copyWith(
inboxItems: state.inboxItems.where(
(element) => element.id != deletedId,
),
),
);
}
void loadSuggestions() {
Future.wait(state.inboxItems
.whereNot((doc) => state.suggestions.containsKey(doc.id))
.map((e) => _documentsApi.findSuggestions(e))).then((results) {
emit(state.copyWith(suggestions: {
...state.suggestions,
for (var r in results) r.documentId!: r
}));
});
}
void acknowledgeHint() {
emit(state.copyWith(isHintAcknowledged: true));
}
@@ -148,4 +229,12 @@ class InboxCubit extends HydratedCubit<InboxState> {
Map<String, dynamic> toJson(InboxState state) {
return state.toJson();
}
@override
Future<void> close() {
_subscriptions.forEach((element) {
element.cancel();
});
return super.close();
}
}

View File

@@ -5,17 +5,24 @@ import 'package:json_annotation/json_annotation.dart';
part 'inbox_state.g.dart';
@JsonSerializable()
@JsonSerializable(
ignoreUnannotated: true,
)
class InboxState with EquatableMixin {
@JsonKey(ignore: true)
final bool isLoaded;
@JsonKey(ignore: true)
final Iterable<int> inboxTags;
@JsonKey(ignore: true)
final Iterable<DocumentModel> inboxItems;
final Map<int, Tag> availableTags;
final Map<int, DocumentType> availableDocumentTypes;
final Map<int, Correspondent> availableCorrespondents;
final Map<int, FieldSuggestions> suggestions;
@JsonKey()
final bool isHintAcknowledged;
const InboxState({
@@ -23,6 +30,10 @@ class InboxState with EquatableMixin {
this.inboxTags = const [],
this.inboxItems = const [],
this.isHintAcknowledged = false,
this.availableTags = const {},
this.availableDocumentTypes = const {},
this.availableCorrespondents = const {},
this.suggestions = const {},
});
@override
@@ -31,6 +42,10 @@ class InboxState with EquatableMixin {
inboxTags,
inboxItems,
isHintAcknowledged,
availableTags,
availableDocumentTypes,
availableCorrespondents,
suggestions,
];
InboxState copyWith({
@@ -38,12 +53,22 @@ class InboxState with EquatableMixin {
Iterable<int>? inboxTags,
Iterable<DocumentModel>? inboxItems,
bool? isHintAcknowledged,
Map<int, Tag>? availableTags,
Map<int, Correspondent>? availableCorrespondents,
Map<int, DocumentType>? availableDocumentTypes,
Map<int, FieldSuggestions>? suggestions,
}) {
return InboxState(
isLoaded: isLoaded ?? this.isLoaded,
inboxItems: inboxItems ?? this.inboxItems,
inboxTags: inboxTags ?? this.inboxTags,
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
availableCorrespondents:
availableCorrespondents ?? this.availableCorrespondents,
availableDocumentTypes:
availableDocumentTypes ?? this.availableDocumentTypes,
availableTags: availableTags ?? this.availableTags,
suggestions: suggestions ?? this.suggestions,
);
}

View File

@@ -24,7 +24,6 @@ class InboxPage extends StatefulWidget {
class _InboxPageState extends State<InboxPage> {
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@override
void initState() {
super.initState();
@@ -40,9 +39,8 @@ class _InboxPageState extends State<InboxPage> {
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(14),
child: BlocBuilder<InboxCubit, InboxState>(
actions: [
BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerRight,
@@ -59,8 +57,8 @@ class _InboxPageState extends State<InboxPage> {
),
);
},
).paddedSymmetrically(horizontal: 8.0),
),
).paddedSymmetrically(horizontal: 8)
],
),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
@@ -79,7 +77,11 @@ class _InboxPageState extends State<InboxPage> {
);
},
),
body: BlocBuilder<InboxCubit, InboxState>(
body: BlocConsumer<InboxCubit, InboxState>(
listenWhen: (previous, current) =>
!previous.isLoaded && current.isLoaded,
listener: (context, state) =>
context.read<InboxCubit>().loadSuggestions(),
builder: (context, state) {
if (!state.isLoaded) {
return const DocumentsListLoadingWidget();
@@ -121,7 +123,8 @@ class _InboxPageState extends State<InboxPage> {
],
)
.flattened
.toList();
.toList()
..add(const SliverToBoxAdapter(child: SizedBox(height: 78)));
return RefreshIndicator(
onRefresh: () => context.read<InboxCubit>().initializeInbox(),

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
@@ -8,116 +7,35 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi
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/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:badges/badges.dart' as b;
import 'package:paperless_mobile/extensions/string_extensions.dart';
class InboxItem extends StatelessWidget {
class InboxItem extends StatefulWidget {
static const _a4AspectRatio = 1 / 1.4142;
final void Function(DocumentModel model) onDocumentUpdated;
final DocumentModel document;
const InboxItem({
super.key,
required this.document,
required this.onDocumentUpdated,
});
@override
State<InboxItem> createState() => _InboxItemState();
}
class _InboxItemState extends State<InboxItem> {
bool _isAsnAssignLoading = false;
@override
Widget build(BuildContext context) {
return ListTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IntrinsicHeight(
child: Wrap(
direction: Axis.horizontal,
children: [
Row(
children: [
Text(
document.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge,
)
],
),
Row(
children: [],
),
],
),
),
],
),
isThreeLine: true,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: DocumentPreview(
id: document.id,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: false,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.person_outline,
size: Theme.of(context).textTheme.bodySmall?.fontSize,
),
Flexible(
child: LabelText<Correspondent, CorrespondentRepositoryState>(
id: document.correspondent,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.description_outlined,
size: Theme.of(context).textTheme.bodySmall?.fontSize,
),
Flexible(
child: LabelText<DocumentType, DocumentTypeRepositoryState>(
id: document.documentType,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
TagsWidget(
tagIds: document.tags,
isMultiLine: false,
isClickable: false,
isSelectedPredicate: (_) => false,
showShortNames: true,
dense: true,
),
],
),
trailing: document.archiveSerialNumber != null
? Text(
document.archiveSerialNumber!.toString(),
style: Theme.of(context).textTheme.bodySmall,
)
: null,
return GestureDetector(
onTap: () async {
final returnedDocument = await Navigator.push<DocumentModel?>(
context,
@@ -125,7 +43,7 @@ class InboxItem extends StatelessWidget {
builder: (context) => BlocProvider(
create: (context) => DocumentDetailsCubit(
context.read<PaperlessDocumentsApi>(),
document,
widget.document,
),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(
@@ -136,9 +54,232 @@ class InboxItem extends StatelessWidget {
),
);
if (returnedDocument != null) {
onDocumentUpdated(returnedDocument);
widget.onDocumentUpdated(returnedDocument);
}
},
child: Container(
padding: const EdgeInsets.all(4),
height: 180,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Row(
children: [
AspectRatio(
aspectRatio: InboxItem._a4AspectRatio,
child: DocumentPreview(
id: widget.document.id,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: false,
),
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
const Spacer(),
_buildCorrespondent(context),
_buildDocumentType(context),
const Spacer(),
_buildTags(),
],
).padded(),
),
],
),
),
SizedBox(
height: 48,
child: _buildActions(context),
),
],
).padded(),
),
);
}
Widget _buildActions(BuildContext context) {
final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32),
);
final actions = [
_buildAssignAsnAction(chipShape, context),
const SizedBox(width: 4.0),
ActionChip(
avatar: const Icon(Icons.delete_outline),
shape: chipShape,
label: const Text("Delete document"),
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) =>
DeleteDocumentConfirmationDialog(document: widget.document),
) ??
false;
if (shouldDelete) {
context.read<InboxCubit>().deleteDocument(widget.document);
}
},
),
];
return BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return ListView(
scrollDirection: Axis.horizontal,
children: [
...actions,
if (state.suggestions[widget.document.id] != null) ...[
SizedBox(width: 4),
..._buildSuggestionChips(
chipShape,
state.suggestions[widget.document.id]!,
state,
)
]
],
);
},
);
}
ActionChip _buildAssignAsnAction(
RoundedRectangleBorder chipShape,
BuildContext context,
) {
final hasAsn = widget.document.archiveSerialNumber != null;
return ActionChip(
avatar: _isAsnAssignLoading
? const CircularProgressIndicator()
: hasAsn
? null
: const Icon(Icons.archive_outlined),
shape: chipShape,
label: hasAsn
? Text(
'${S.of(context).documentArchiveSerialNumberPropertyShortLabel} #${widget.document.archiveSerialNumber}',
)
: const Text("Assign ASN"),
onPressed: !hasAsn
? () {
setState(() {
_isAsnAssignLoading = true;
});
context
.read<InboxCubit>()
.assignAsn(widget.document)
.whenComplete(
() => setState(() => _isAsnAssignLoading = false),
);
}
: null,
);
}
TagsWidget _buildTags() {
return TagsWidget(
tagIds: widget.document.tags,
isMultiLine: false,
isClickable: false,
isSelectedPredicate: (_) => false,
showShortNames: true,
dense: true,
);
}
Row _buildDocumentType(BuildContext context) {
return _buildTextWithLeadingIcon(
Icon(
Icons.description_outlined,
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
LabelText<DocumentType, DocumentTypeRepositoryState>(
id: widget.document.documentType,
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
),
);
}
Row _buildCorrespondent(BuildContext context) {
return _buildTextWithLeadingIcon(
Icon(
Icons.person_outline,
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
LabelText<Correspondent, CorrespondentRepositoryState>(
id: widget.document.correspondent,
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
),
);
}
Text _buildTitle() {
return Text(
widget.document.title,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: Theme.of(context).textTheme.titleSmall,
);
}
Row _buildTextWithLeadingIcon(Icon icon, Widget child) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
icon,
const SizedBox(width: 2),
Flexible(
child: child,
),
],
);
}
List<Widget> _buildSuggestionChips(
OutlinedBorder chipShape,
FieldSuggestions suggestions,
InboxState state,
) {
return [
...suggestions.correspondents
.map(
(e) => ActionChip(
avatar: const Icon(Icons.person_outline),
shape: chipShape,
label: Text(state.availableCorrespondents[e]?.name ?? ''),
onPressed: () {
context
.read<InboxCubit>()
.updateDocument(widget.document.copyWith(
correspondent: e,
overwriteCorrespondent: true,
));
},
),
)
.toList(),
...suggestions.documentTypes
.map(
(e) => ActionChip(
avatar: const Icon(Icons.description_outlined),
shape: chipShape,
label: Text(state.availableDocumentTypes[e]?.name ?? ''),
onPressed: () {
context
.read<InboxCubit>()
.updateDocument(widget.document.copyWith(
documentType: e,
overwriteDocumentType: true,
));
},
),
)
.toList(),
];
}
}

View File

@@ -194,7 +194,8 @@ class _TagFormFieldState extends State<TagFormField> {
Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
spacing: 8.0,
spacing: 4.0,
runSpacing: 4.0,
children: ((field.value as IdsTagsQuery).queries)
.map(
(query) => _buildTag(

View File

@@ -4,6 +4,7 @@ part 'field_suggestions.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class FieldSuggestions {
final int? documentId;
final Iterable<int> correspondents;
final Iterable<int> tags;
final Iterable<int> documentTypes;
@@ -11,6 +12,7 @@ class FieldSuggestions {
final Iterable<DateTime> dates;
const FieldSuggestions({
this.documentId,
this.correspondents = const [],
this.tags = const [],
this.documentTypes = const [],
@@ -38,6 +40,15 @@ class FieldSuggestions {
(storagePaths.isNotEmpty ? 1 : 0) +
(dates.isNotEmpty ? 1 : 0);
FieldSuggestions forDocumentId(int id) => FieldSuggestions(
documentId: id,
correspondents: correspondents,
dates: dates,
documentTypes: documentTypes,
tags: tags,
storagePaths: storagePaths,
);
factory FieldSuggestions.fromJson(Map<String, dynamic> json) =>
_$FieldSuggestionsFromJson(json);

View File

@@ -8,6 +8,7 @@ part of 'field_suggestions.dart';
FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
FieldSuggestions(
documentId: json['document_id'] as int?,
correspondents:
(json['correspondents'] as List<dynamic>?)?.map((e) => e as int) ??
const [],
@@ -25,6 +26,7 @@ FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$FieldSuggestionsToJson(FieldSuggestions instance) =>
<String, dynamic>{
'document_id': instance.documentId,
'correspondents': instance.correspondents.toList(),
'tags': instance.tags.toList(),
'document_types': instance.documentTypes.toList(),

View File

@@ -259,7 +259,8 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response =
await client.get("/api/documents/${document.id}/suggestions/");
if (response.statusCode == 200) {
return FieldSuggestions.fromJson(response.data);
return FieldSuggestions.fromJson(response.data)
.forDocumentId(document.id);
}
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
} on DioError catch (err) {