mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 04:07:57 -06:00
Reworked inbox, added more information and allow suggestions to be directly clicked
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.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/highlighted_text.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/offline_widget.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/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/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/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/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:badges/badges.dart' as b;
|
import 'package:badges/badges.dart' as b;
|
||||||
|
|
||||||
|
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
|
||||||
|
|
||||||
class DocumentDetailsPage extends StatefulWidget {
|
class DocumentDetailsPage extends StatefulWidget {
|
||||||
final bool allowEdit;
|
final bool allowEdit;
|
||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
@@ -284,7 +288,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
content: document.archiveSerialNumber != null
|
content: document.archiveSerialNumber != null
|
||||||
? Text(document.archiveSerialNumber.toString())
|
? Text(document.archiveSerialNumber.toString())
|
||||||
: TextButton.icon(
|
: TextButton.icon(
|
||||||
icon: const Icon(Icons.archive),
|
icon: const Icon(Icons.archive_outlined),
|
||||||
label: Text(S
|
label: Text(S
|
||||||
.of(context)
|
.of(context)
|
||||||
.documentDetailsPageAssignAsnButtonLabel),
|
.documentDetailsPageAssignAsnButtonLabel),
|
||||||
@@ -369,37 +373,35 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
_DetailsItem(
|
_DetailsItem(
|
||||||
|
label: S.of(context).documentTitlePropertyLabel,
|
||||||
content: HighlightedText(
|
content: HighlightedText(
|
||||||
text: document.title,
|
text: document.title,
|
||||||
highlights: widget.titleAndContentQueryString?.split(" ") ?? [],
|
highlights: widget.titleAndContentQueryString?.split(" ") ?? [],
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
label: S.of(context).documentTitlePropertyLabel,
|
|
||||||
).paddedOnly(bottom: 16),
|
).paddedOnly(bottom: 16),
|
||||||
_DetailsItem.text(
|
_DetailsItem.text(
|
||||||
DateFormat.yMMMd().format(document.created),
|
DateFormat.yMMMMd().format(document.created),
|
||||||
context: context,
|
context: context,
|
||||||
label: S.of(context).documentCreatedPropertyLabel,
|
label: S.of(context).documentCreatedPropertyLabel,
|
||||||
).paddedSymmetrically(vertical: 16),
|
).paddedSymmetrically(vertical: 16),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: document.documentType != null,
|
visible: document.documentType != null,
|
||||||
child: _DetailsItem(
|
child: _DetailsItem(
|
||||||
content: DocumentTypeWidget(
|
|
||||||
textStyle: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
isClickable: widget.isLabelClickable,
|
|
||||||
documentTypeId: document.documentType,
|
|
||||||
),
|
|
||||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||||
|
content: LabelText<DocumentType, DocumentTypeRepositoryState>(
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
id: document.documentType,
|
||||||
|
),
|
||||||
).paddedSymmetrically(vertical: 16),
|
).paddedSymmetrically(vertical: 16),
|
||||||
),
|
),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: document.correspondent != null,
|
visible: document.correspondent != null,
|
||||||
child: _DetailsItem(
|
child: _DetailsItem(
|
||||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
content: CorrespondentWidget(
|
content: LabelText<Correspondent, CorrespondentRepositoryState>(
|
||||||
textStyle: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
isClickable: widget.isLabelClickable,
|
id: document.correspondent,
|
||||||
correspondentId: document.correspondent,
|
|
||||||
),
|
),
|
||||||
).paddedSymmetrically(vertical: 16),
|
).paddedSymmetrically(vertical: 16),
|
||||||
),
|
),
|
||||||
@@ -521,8 +523,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
class _DetailsItem extends StatelessWidget {
|
class _DetailsItem extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final Widget content;
|
final Widget content;
|
||||||
const _DetailsItem({Key? key, required this.label, required this.content})
|
const _DetailsItem({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
required this.label,
|
||||||
|
required this.content,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -545,7 +550,10 @@ class _DetailsItem extends StatelessWidget {
|
|||||||
String text, {
|
String text, {
|
||||||
required this.label,
|
required this.label,
|
||||||
required BuildContext context,
|
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 {
|
class ColoredTabBar extends Container implements PreferredSizeWidget {
|
||||||
|
|||||||
@@ -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/document_upload/view/document_upload_preparation_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/documents_page.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/bottom_navigation_bar.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
||||||
@@ -145,6 +146,71 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return MultiBlocListener(
|
||||||
listeners: [
|
listeners: [
|
||||||
BlocListener<ConnectivityCubit, ConnectivityState>(
|
BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||||
@@ -172,96 +238,35 @@ class _HomePageState extends State<HomePage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
key: rootScaffoldKey,
|
||||||
drawer: const InfoDrawer(),
|
drawer: const InfoDrawer(),
|
||||||
body: Row(children: [
|
body: Row(
|
||||||
NavigationRail(
|
children: [
|
||||||
labelType: NavigationRailLabelType.all,
|
NavigationRail(
|
||||||
destinations: [
|
labelType: NavigationRailLabelType.all,
|
||||||
NavigationRailDestination(
|
destinations: destinations
|
||||||
icon: const Icon(Icons.description_outlined),
|
.map((e) => e.toNavigationRailDestination())
|
||||||
selectedIcon: Icon(
|
.toList(),
|
||||||
Icons.description,
|
selectedIndex: _currentIndex,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
onDestinationSelected: _onNavigationChanged,
|
||||||
),
|
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
BlocProvider.value(
|
const VerticalDivider(thickness: 1, width: 1),
|
||||||
value: _scannerCubit,
|
Expanded(
|
||||||
child: const ScannerPage(),
|
child: routes[_currentIndex],
|
||||||
),
|
),
|
||||||
const LabelsPage(),
|
],
|
||||||
][_currentIndex]),
|
),
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
key: rootScaffoldKey,
|
||||||
bottomNavigationBar: BottomNavBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
|
elevation: 4.0,
|
||||||
selectedIndex: _currentIndex,
|
selectedIndex: _currentIndex,
|
||||||
onNavigationChanged: _onNavigationChanged,
|
onDestinationSelected: _onNavigationChanged,
|
||||||
|
destinations:
|
||||||
|
destinations.map((e) => e.toNavigationDestination()).toList(),
|
||||||
),
|
),
|
||||||
drawer: const InfoDrawer(),
|
drawer: const InfoDrawer(),
|
||||||
body: [
|
body: routes[_currentIndex],
|
||||||
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],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
37
lib/features/home/view/route_description.dart
Normal file
37
lib/features/home/view/route_description.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,22 @@ class BottomNavBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
label: S.of(context).bottomNavLabelsPageLabel,
|
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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,8 +307,10 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
|||||||
builder: (_) => LabelRepositoriesProvider(
|
builder: (_) => LabelRepositoriesProvider(
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) => InboxCubit(
|
create: (context) => InboxCubit(
|
||||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
context.read(),
|
||||||
context.read<PaperlessDocumentsApi>(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
)..initializeInbox(),
|
)..initializeInbox(),
|
||||||
child: const InboxPage(),
|
child: const InboxPage(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,16 +1,64 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/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/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';
|
||||||
|
|
||||||
class InboxCubit extends HydratedCubit<InboxState> {
|
class InboxCubit extends HydratedCubit<InboxState> {
|
||||||
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
|
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
|
||||||
|
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||||
|
_correspondentRepository;
|
||||||
|
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
||||||
|
_documentTypeRepository;
|
||||||
|
|
||||||
final PaperlessDocumentsApi _documentsApi;
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
|
|
||||||
InboxCubit(this._tagsRepository, this._documentsApi)
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
: super(const InboxState());
|
|
||||||
|
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).
|
/// 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() {
|
void acknowledgeHint() {
|
||||||
emit(state.copyWith(isHintAcknowledged: true));
|
emit(state.copyWith(isHintAcknowledged: true));
|
||||||
}
|
}
|
||||||
@@ -148,4 +229,12 @@ class InboxCubit extends HydratedCubit<InboxState> {
|
|||||||
Map<String, dynamic> toJson(InboxState state) {
|
Map<String, dynamic> toJson(InboxState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_subscriptions.forEach((element) {
|
||||||
|
element.cancel();
|
||||||
|
});
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,24 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
|
|
||||||
part 'inbox_state.g.dart';
|
part 'inbox_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable(
|
||||||
|
ignoreUnannotated: true,
|
||||||
|
)
|
||||||
class InboxState with EquatableMixin {
|
class InboxState with EquatableMixin {
|
||||||
@JsonKey(ignore: true)
|
|
||||||
final bool isLoaded;
|
final bool isLoaded;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
final Iterable<int> inboxTags;
|
final Iterable<int> inboxTags;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
final Iterable<DocumentModel> inboxItems;
|
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;
|
final bool isHintAcknowledged;
|
||||||
|
|
||||||
const InboxState({
|
const InboxState({
|
||||||
@@ -23,6 +30,10 @@ class InboxState with EquatableMixin {
|
|||||||
this.inboxTags = const [],
|
this.inboxTags = const [],
|
||||||
this.inboxItems = const [],
|
this.inboxItems = const [],
|
||||||
this.isHintAcknowledged = false,
|
this.isHintAcknowledged = false,
|
||||||
|
this.availableTags = const {},
|
||||||
|
this.availableDocumentTypes = const {},
|
||||||
|
this.availableCorrespondents = const {},
|
||||||
|
this.suggestions = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,6 +42,10 @@ class InboxState with EquatableMixin {
|
|||||||
inboxTags,
|
inboxTags,
|
||||||
inboxItems,
|
inboxItems,
|
||||||
isHintAcknowledged,
|
isHintAcknowledged,
|
||||||
|
availableTags,
|
||||||
|
availableDocumentTypes,
|
||||||
|
availableCorrespondents,
|
||||||
|
suggestions,
|
||||||
];
|
];
|
||||||
|
|
||||||
InboxState copyWith({
|
InboxState copyWith({
|
||||||
@@ -38,12 +53,22 @@ class InboxState with EquatableMixin {
|
|||||||
Iterable<int>? inboxTags,
|
Iterable<int>? inboxTags,
|
||||||
Iterable<DocumentModel>? inboxItems,
|
Iterable<DocumentModel>? inboxItems,
|
||||||
bool? isHintAcknowledged,
|
bool? isHintAcknowledged,
|
||||||
|
Map<int, Tag>? availableTags,
|
||||||
|
Map<int, Correspondent>? availableCorrespondents,
|
||||||
|
Map<int, DocumentType>? availableDocumentTypes,
|
||||||
|
Map<int, FieldSuggestions>? suggestions,
|
||||||
}) {
|
}) {
|
||||||
return InboxState(
|
return InboxState(
|
||||||
isLoaded: isLoaded ?? this.isLoaded,
|
isLoaded: isLoaded ?? this.isLoaded,
|
||||||
inboxItems: inboxItems ?? this.inboxItems,
|
inboxItems: inboxItems ?? this.inboxItems,
|
||||||
inboxTags: inboxTags ?? this.inboxTags,
|
inboxTags: inboxTags ?? this.inboxTags,
|
||||||
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
|
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
|
||||||
|
availableCorrespondents:
|
||||||
|
availableCorrespondents ?? this.availableCorrespondents,
|
||||||
|
availableDocumentTypes:
|
||||||
|
availableDocumentTypes ?? this.availableDocumentTypes,
|
||||||
|
availableTags: availableTags ?? this.availableTags,
|
||||||
|
suggestions: suggestions ?? this.suggestions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class InboxPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _InboxPageState extends State<InboxPage> {
|
class _InboxPageState extends State<InboxPage> {
|
||||||
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -40,9 +39,8 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
bottom: PreferredSize(
|
actions: [
|
||||||
preferredSize: const Size.fromHeight(14),
|
BlocBuilder<InboxCubit, InboxState>(
|
||||||
child: BlocBuilder<InboxCubit, InboxState>(
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -59,8 +57,8 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).paddedSymmetrically(horizontal: 8.0),
|
).paddedSymmetrically(horizontal: 8)
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
|
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
|
||||||
builder: (context, state) {
|
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) {
|
builder: (context, state) {
|
||||||
if (!state.isLoaded) {
|
if (!state.isLoaded) {
|
||||||
return const DocumentsListLoadingWidget();
|
return const DocumentsListLoadingWidget();
|
||||||
@@ -121,7 +123,8 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.flattened
|
.flattened
|
||||||
.toList();
|
.toList()
|
||||||
|
..add(const SliverToBoxAdapter(child: SizedBox(height: 78)));
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () => context.read<InboxCubit>().initializeInbox(),
|
onRefresh: () => context.read<InboxCubit>().initializeInbox(),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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:intl/intl.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.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/provider/label_repositories_provider.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';
|
||||||
@@ -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/extensions/flutter_extensions.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/documents/view/widgets/delete_document_confirmation_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.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/inbox/bloc/inbox_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package: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;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
|
|
||||||
final void Function(DocumentModel model) onDocumentUpdated;
|
final void Function(DocumentModel model) onDocumentUpdated;
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
|
|
||||||
const InboxItem({
|
const InboxItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.document,
|
required this.document,
|
||||||
required this.onDocumentUpdated,
|
required this.onDocumentUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InboxItem> createState() => _InboxItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InboxItemState extends State<InboxItem> {
|
||||||
|
bool _isAsnAssignLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return GestureDetector(
|
||||||
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,
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final returnedDocument = await Navigator.push<DocumentModel?>(
|
final returnedDocument = await Navigator.push<DocumentModel?>(
|
||||||
context,
|
context,
|
||||||
@@ -125,7 +43,7 @@ class InboxItem extends StatelessWidget {
|
|||||||
builder: (context) => BlocProvider(
|
builder: (context) => BlocProvider(
|
||||||
create: (context) => DocumentDetailsCubit(
|
create: (context) => DocumentDetailsCubit(
|
||||||
context.read<PaperlessDocumentsApi>(),
|
context.read<PaperlessDocumentsApi>(),
|
||||||
document,
|
widget.document,
|
||||||
),
|
),
|
||||||
child: const LabelRepositoriesProvider(
|
child: const LabelRepositoriesProvider(
|
||||||
child: DocumentDetailsPage(
|
child: DocumentDetailsPage(
|
||||||
@@ -136,9 +54,232 @@ class InboxItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (returnedDocument != null) {
|
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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.start,
|
alignment: WrapAlignment.start,
|
||||||
runAlignment: WrapAlignment.start,
|
runAlignment: WrapAlignment.start,
|
||||||
spacing: 8.0,
|
spacing: 4.0,
|
||||||
|
runSpacing: 4.0,
|
||||||
children: ((field.value as IdsTagsQuery).queries)
|
children: ((field.value as IdsTagsQuery).queries)
|
||||||
.map(
|
.map(
|
||||||
(query) => _buildTag(
|
(query) => _buildTag(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ part 'field_suggestions.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||||
class FieldSuggestions {
|
class FieldSuggestions {
|
||||||
|
final int? documentId;
|
||||||
final Iterable<int> correspondents;
|
final Iterable<int> correspondents;
|
||||||
final Iterable<int> tags;
|
final Iterable<int> tags;
|
||||||
final Iterable<int> documentTypes;
|
final Iterable<int> documentTypes;
|
||||||
@@ -11,6 +12,7 @@ class FieldSuggestions {
|
|||||||
final Iterable<DateTime> dates;
|
final Iterable<DateTime> dates;
|
||||||
|
|
||||||
const FieldSuggestions({
|
const FieldSuggestions({
|
||||||
|
this.documentId,
|
||||||
this.correspondents = const [],
|
this.correspondents = const [],
|
||||||
this.tags = const [],
|
this.tags = const [],
|
||||||
this.documentTypes = const [],
|
this.documentTypes = const [],
|
||||||
@@ -38,6 +40,15 @@ class FieldSuggestions {
|
|||||||
(storagePaths.isNotEmpty ? 1 : 0) +
|
(storagePaths.isNotEmpty ? 1 : 0) +
|
||||||
(dates.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) =>
|
factory FieldSuggestions.fromJson(Map<String, dynamic> json) =>
|
||||||
_$FieldSuggestionsFromJson(json);
|
_$FieldSuggestionsFromJson(json);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ part of 'field_suggestions.dart';
|
|||||||
|
|
||||||
FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
|
FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
|
||||||
FieldSuggestions(
|
FieldSuggestions(
|
||||||
|
documentId: json['document_id'] as int?,
|
||||||
correspondents:
|
correspondents:
|
||||||
(json['correspondents'] as List<dynamic>?)?.map((e) => e as int) ??
|
(json['correspondents'] as List<dynamic>?)?.map((e) => e as int) ??
|
||||||
const [],
|
const [],
|
||||||
@@ -25,6 +26,7 @@ FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
|
|||||||
|
|
||||||
Map<String, dynamic> _$FieldSuggestionsToJson(FieldSuggestions instance) =>
|
Map<String, dynamic> _$FieldSuggestionsToJson(FieldSuggestions instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
|
'document_id': instance.documentId,
|
||||||
'correspondents': instance.correspondents.toList(),
|
'correspondents': instance.correspondents.toList(),
|
||||||
'tags': instance.tags.toList(),
|
'tags': instance.tags.toList(),
|
||||||
'document_types': instance.documentTypes.toList(),
|
'document_types': instance.documentTypes.toList(),
|
||||||
|
|||||||
@@ -259,7 +259,8 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
final response =
|
final response =
|
||||||
await client.get("/api/documents/${document.id}/suggestions/");
|
await client.get("/api/documents/${document.id}/suggestions/");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return FieldSuggestions.fromJson(response.data);
|
return FieldSuggestions.fromJson(response.data)
|
||||||
|
.forDocumentId(document.id);
|
||||||
}
|
}
|
||||||
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
|
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
|
||||||
} on DioError catch (err) {
|
} on DioError catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user