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: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 {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
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(
|
||||
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(),
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user