mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 15:15:50 -06:00
WIP - Implemented similar documents view
This commit is contained in:
@@ -5,30 +5,45 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||||
final List<Widget> above;
|
final List<Widget> beforeWidgets;
|
||||||
final List<Widget> below;
|
final List<Widget> afterWidgets;
|
||||||
static const tags = [" ", " ", " "];
|
|
||||||
static const titleLengths = <double>[double.infinity, 150.0, 200.0];
|
static const _tags = [" ", " ", " "];
|
||||||
static const correspondentLengths = <double>[200.0, 300.0, 150.0];
|
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
||||||
static const fontSize = 16.0;
|
static const _correspondentLengths = <double>[200.0, 300.0, 150.0];
|
||||||
|
static const _fontSize = 16.0;
|
||||||
|
|
||||||
const DocumentsListLoadingWidget({
|
const DocumentsListLoadingWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.above = const [],
|
this.beforeWidgets = const [],
|
||||||
this.below = const [],
|
this.afterWidgets = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
final _random = Random();
|
||||||
children: <Widget>[
|
return CustomScrollView(
|
||||||
...above,
|
slivers: [
|
||||||
...List.generate(25, (idx) {
|
SliverList(
|
||||||
final r = Random(idx);
|
delegate: SliverChildListDelegate(beforeWidgets),
|
||||||
final tagCount = r.nextInt(tags.length + 1);
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return _buildFakeListItem(context, _random);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(delegate: SliverChildListDelegate(afterWidgets))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFakeListItem(BuildContext context, Random random) {
|
||||||
|
final tagCount = random.nextInt(_tags.length + 1);
|
||||||
final correspondentLength =
|
final correspondentLength =
|
||||||
correspondentLengths[r.nextInt(correspondentLengths.length - 1)];
|
_correspondentLengths[random.nextInt(_correspondentLengths.length - 1)];
|
||||||
final titleLength = titleLengths[r.nextInt(titleLengths.length - 1)];
|
final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)];
|
||||||
return Shimmer.fromColors(
|
return Shimmer.fromColors(
|
||||||
baseColor: Theme.of(context).brightness == Brightness.light
|
baseColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[300]!
|
? Colors.grey[300]!
|
||||||
@@ -51,7 +66,7 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
title: Container(
|
title: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
width: correspondentLength,
|
width: correspondentLength,
|
||||||
height: fontSize,
|
height: _fontSize,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
@@ -62,7 +77,7 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
height: fontSize,
|
height: _fontSize,
|
||||||
width: titleLength,
|
width: titleLength,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -71,7 +86,7 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
children: List.generate(
|
children: List.generate(
|
||||||
tagCount,
|
tagCount,
|
||||||
(index) => InputChip(
|
(index) => InputChip(
|
||||||
label: Text(tags[r.nextInt(tags.length)]),
|
label: Text(_tags[random.nextInt(_tags.length)]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddedOnly(top: 4),
|
).paddedOnly(top: 4),
|
||||||
@@ -80,9 +95,5 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
|
||||||
...below,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
class HintCard extends StatelessWidget {
|
class HintCard extends StatelessWidget {
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final double elevation;
|
final double elevation;
|
||||||
|
final IconData hintIcon;
|
||||||
final VoidCallback? onHintAcknowledged;
|
final VoidCallback? onHintAcknowledged;
|
||||||
final bool show;
|
final bool show;
|
||||||
const HintCard({
|
const HintCard({
|
||||||
@@ -13,7 +14,8 @@ class HintCard extends StatelessWidget {
|
|||||||
required this.hintText,
|
required this.hintText,
|
||||||
this.onHintAcknowledged,
|
this.onHintAcknowledged,
|
||||||
this.elevation = 1,
|
this.elevation = 1,
|
||||||
required this.show,
|
this.show = true,
|
||||||
|
this.hintIcon = Icons.tips_and_updates_outlined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,7 +33,7 @@ class HintCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.tips_and_updates_outlined,
|
hintIcon,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
).padded(),
|
).padded(),
|
||||||
Align(
|
Align(
|
||||||
@@ -52,7 +54,7 @@ class HintCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Padding(padding: EdgeInsets.only(bottom: 24)),
|
const Padding(padding: EdgeInsets.only(bottom: 24)),
|
||||||
],
|
],
|
||||||
).padded(),
|
).padded(),
|
||||||
).padded(),
|
).padded(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
|||||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||||
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/similar_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
|
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||||
@@ -23,6 +24,7 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi
|
|||||||
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/features/labels/view/widgets/label_text.dart';
|
||||||
|
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.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';
|
||||||
@@ -31,6 +33,7 @@ import 'package:badges/badges.dart' as b;
|
|||||||
|
|
||||||
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
|
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
|
||||||
|
|
||||||
|
//TODO: Refactor this into several widgets
|
||||||
class DocumentDetailsPage extends StatefulWidget {
|
class DocumentDetailsPage extends StatefulWidget {
|
||||||
final bool allowEdit;
|
final bool allowEdit;
|
||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
@@ -48,6 +51,16 @@ class DocumentDetailsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||||
|
late Future<DocumentMetaData> _metaData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_metaData = context
|
||||||
|
.read<PaperlessDocumentsApi>()
|
||||||
|
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
@@ -57,102 +70,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 3,
|
length: 4,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||||
floatingActionButton: widget.allowEdit
|
floatingActionButton: widget.allowEdit ? _buildAppBar() : null,
|
||||||
? BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
bottomNavigationBar: _buildBottomAppBar(),
|
||||||
builder: (context, state) {
|
|
||||||
final _filteredSuggestions =
|
|
||||||
state.suggestions.documentDifference(state.document);
|
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectivityState) {
|
|
||||||
if (!connectivityState.isConnected) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
return b.Badge(
|
|
||||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
|
||||||
showBadge: _filteredSuggestions.hasSuggestions,
|
|
||||||
child: Tooltip(
|
|
||||||
message:
|
|
||||||
S.of(context).documentDetailsPageEditTooltip,
|
|
||||||
preferBelow: false,
|
|
||||||
verticalOffset: 40,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
child: const Icon(Icons.edit),
|
|
||||||
onPressed: () => _onEdit(state.document),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
badgeContent: Text(
|
|
||||||
'${_filteredSuggestions.suggestionsCount}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
badgeColor: Colors.red,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
bottomNavigationBar:
|
|
||||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return BottomAppBar(
|
|
||||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectivityState) {
|
|
||||||
final isConnected = connectivityState.isConnected;
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
tooltip:
|
|
||||||
S.of(context).documentDetailsPageDeleteTooltip,
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: widget.allowEdit && isConnected
|
|
||||||
? () => _onDelete(state.document)
|
|
||||||
: null,
|
|
||||||
).paddedSymmetrically(horizontal: 4),
|
|
||||||
Tooltip(
|
|
||||||
message:
|
|
||||||
S.of(context).documentDetailsPageDownloadTooltip,
|
|
||||||
child: DocumentDownloadButton(
|
|
||||||
document: state.document,
|
|
||||||
enabled: isConnected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip:
|
|
||||||
S.of(context).documentDetailsPagePreviewTooltip,
|
|
||||||
icon: const Icon(Icons.visibility),
|
|
||||||
onPressed: isConnected
|
|
||||||
? () => _onOpen(state.document)
|
|
||||||
: null,
|
|
||||||
).paddedOnly(right: 4.0),
|
|
||||||
IconButton(
|
|
||||||
tooltip: S
|
|
||||||
.of(context)
|
|
||||||
.documentDetailsPageOpenInSystemViewerTooltip,
|
|
||||||
icon: const Icon(Icons.open_in_new),
|
|
||||||
onPressed:
|
|
||||||
isConnected ? _onOpenFileInSystemViewer : null,
|
|
||||||
).paddedOnly(right: 4.0),
|
|
||||||
IconButton(
|
|
||||||
tooltip:
|
|
||||||
S.of(context).documentDetailsPageShareTooltip,
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: isConnected
|
|
||||||
? () => _onShare(state.document)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
@@ -180,6 +102,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
tabBar: TabBar(
|
tabBar: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -208,6 +131,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
.onPrimaryContainer),
|
.onPrimaryContainer),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S
|
||||||
|
.of(context)
|
||||||
|
.documentDetailsPageTabSimilarDocumentsLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -215,7 +150,12 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
],
|
],
|
||||||
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return TabBarView(
|
return BlocProvider(
|
||||||
|
create: (context) => SimilarDocumentsCubit(
|
||||||
|
context.read(),
|
||||||
|
documentId: state.document.id,
|
||||||
|
),
|
||||||
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_buildDocumentOverview(
|
_buildDocumentOverview(
|
||||||
state.document,
|
state.document,
|
||||||
@@ -227,7 +167,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
_buildDocumentMetaDataView(
|
_buildDocumentMetaDataView(
|
||||||
state.document,
|
state.document,
|
||||||
),
|
),
|
||||||
|
_buildSimilarDocumentsView(),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
).paddedSymmetrically(horizontal: 8);
|
).paddedSymmetrically(horizontal: 8);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -237,6 +179,94 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildAppBar() {
|
||||||
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final _filteredSuggestions =
|
||||||
|
state.suggestions.documentDifference(state.document);
|
||||||
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
|
builder: (context, connectivityState) {
|
||||||
|
if (!connectivityState.isConnected) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
return b.Badge(
|
||||||
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
|
showBadge: _filteredSuggestions.hasSuggestions,
|
||||||
|
child: Tooltip(
|
||||||
|
message: S.of(context).documentDetailsPageEditTooltip,
|
||||||
|
preferBelow: false,
|
||||||
|
verticalOffset: 40,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.edit),
|
||||||
|
onPressed: () => _onEdit(state.document),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
badgeContent: Text(
|
||||||
|
'${_filteredSuggestions.suggestionsCount}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
badgeColor: Colors.red,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildBottomAppBar() {
|
||||||
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return BottomAppBar(
|
||||||
|
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
|
builder: (context, connectivityState) {
|
||||||
|
final isConnected = connectivityState.isConnected;
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context).documentDetailsPageDeleteTooltip,
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: widget.allowEdit && isConnected
|
||||||
|
? () => _onDelete(state.document)
|
||||||
|
: null,
|
||||||
|
).paddedSymmetrically(horizontal: 4),
|
||||||
|
Tooltip(
|
||||||
|
message: S.of(context).documentDetailsPageDownloadTooltip,
|
||||||
|
child: DocumentDownloadButton(
|
||||||
|
document: state.document,
|
||||||
|
enabled: isConnected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context).documentDetailsPagePreviewTooltip,
|
||||||
|
icon: const Icon(Icons.visibility),
|
||||||
|
onPressed:
|
||||||
|
isConnected ? () => _onOpen(state.document) : null,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S
|
||||||
|
.of(context)
|
||||||
|
.documentDetailsPageOpenInSystemViewerTooltip,
|
||||||
|
icon: const Icon(Icons.open_in_new),
|
||||||
|
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context).documentDetailsPageShareTooltip,
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
onPressed:
|
||||||
|
isConnected ? () => _onShare(state.document) : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onEdit(DocumentModel document) async {
|
Future<void> _onEdit(DocumentModel document) async {
|
||||||
{
|
{
|
||||||
final cubit = context.read<DocumentDetailsCubit>();
|
final cubit = context.read<DocumentDetailsCubit>();
|
||||||
@@ -306,7 +336,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return FutureBuilder<DocumentMetaData>(
|
return FutureBuilder<DocumentMetaData>(
|
||||||
future: context.read<PaperlessDocumentsApi>().getMetaData(document),
|
future: _metaData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -465,34 +495,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
child: TagsWidget(
|
child: TagsWidget(
|
||||||
isClickable: widget.isLabelClickable,
|
isClickable: widget.isLabelClickable,
|
||||||
tagIds: document.tags,
|
tagIds: document.tags,
|
||||||
onTagSelected: (int tagId) {},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddedSymmetrically(vertical: 16),
|
).paddedSymmetrically(vertical: 16),
|
||||||
),
|
),
|
||||||
// _separator(),
|
|
||||||
// FutureBuilder<List<SimilarDocumentModel>>(
|
|
||||||
// future: getIt<DocumentRepository>().findSimilar(document.id),
|
|
||||||
// builder: (context, snapshot) {
|
|
||||||
// if (!snapshot.hasData) {
|
|
||||||
// return CircularProgressIndicator();
|
|
||||||
// }
|
|
||||||
// return ExpansionTile(
|
|
||||||
// tilePadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
// title: Text(
|
|
||||||
// S.of(context).documentDetailsPageSimilarDocumentsLabel,
|
|
||||||
// style:
|
|
||||||
// Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
// ),
|
|
||||||
// children: snapshot.data!
|
|
||||||
// .map((e) => DocumentListItem(
|
|
||||||
// document: e,
|
|
||||||
// onTap: (doc) {},
|
|
||||||
// isSelected: false,
|
|
||||||
// isAtLeastOneSelected: false))
|
|
||||||
// .toList(),
|
|
||||||
// );
|
|
||||||
// }),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -558,6 +564,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
' ' +
|
' ' +
|
||||||
suffixes[i];
|
suffixes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSimilarDocumentsView() {
|
||||||
|
return const SimilarDocumentsView();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailsItem extends StatelessWidget {
|
class _DetailsItem extends StatelessWidget {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
|
||||||
|
class SimilarDocumentsView extends StatefulWidget {
|
||||||
|
const SimilarDocumentsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SimilarDocumentsView> createState() => _SimilarDocumentsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_listenForLoadNewData);
|
||||||
|
try {
|
||||||
|
context.read<SimilarDocumentsCubit>().initialize();
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.removeListener(_listenForLoadNewData);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenForLoadNewData() async {
|
||||||
|
final currState = context.read<SimilarDocumentsCubit>().state;
|
||||||
|
if (_scrollController.offset >=
|
||||||
|
_scrollController.position.maxScrollExtent * 0.75 &&
|
||||||
|
!currState.isLoading &&
|
||||||
|
!currState.isLastPageLoaded) {
|
||||||
|
try {
|
||||||
|
await context.read<SimilarDocumentsCubit>().loadMore();
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const earlyPreviewHintCard = HintCard(
|
||||||
|
hintIcon: Icons.construction,
|
||||||
|
hintText: "This view is still work in progress.",
|
||||||
|
);
|
||||||
|
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (!state.hasLoaded) {
|
||||||
|
return const DocumentsListLoadingWidget(
|
||||||
|
beforeWidgets: [earlyPreviewHintCard],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state.documents.isEmpty) {
|
||||||
|
return DocumentsEmptyState(
|
||||||
|
state: state,
|
||||||
|
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter(
|
||||||
|
filter: DocumentFilter.initial.copyWith(
|
||||||
|
moreLike: () =>
|
||||||
|
context.read<SimilarDocumentsCubit>().documentId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
const SliverToBoxAdapter(child: earlyPreviewHintCard),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
childCount: state.documents.length,
|
||||||
|
(context, index) => DocumentListItem(
|
||||||
|
document: state.documents[index],
|
||||||
|
enableHeroAnimation: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,13 +66,17 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
..addListener(_listenForLoadNewData);
|
..addListener(_listenForLoadNewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenForLoadNewData() {
|
void _listenForLoadNewData() async {
|
||||||
final currState = context.read<DocumentsCubit>().state;
|
final currState = context.read<DocumentsCubit>().state;
|
||||||
if (_scrollController.offset >=
|
if (_scrollController.offset >=
|
||||||
_scrollController.position.maxScrollExtent * 0.75 &&
|
_scrollController.position.maxScrollExtent * 0.75 &&
|
||||||
!currState.isLoading &&
|
!currState.isLoading &&
|
||||||
!currState.isLastPageLoaded) {
|
!currState.isLastPageLoaded) {
|
||||||
_loadNewPage();
|
try {
|
||||||
|
await context.read<DocumentsCubit>().loadMore();
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +357,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
.equals(previous.documents, current.documents) ||
|
.equals(previous.documents, current.documents) ||
|
||||||
previous.selectedIds != current.selectedIds,
|
previous.selectedIds != current.selectedIds,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Some ugly tricks to make it work with bloc, update pageController
|
|
||||||
|
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
if (state.hasLoaded && state.documents.isEmpty) {
|
||||||
return DocumentsEmptyState(
|
return DocumentsEmptyState(
|
||||||
state: state,
|
state: state,
|
||||||
@@ -491,14 +493,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadNewPage() async {
|
|
||||||
try {
|
|
||||||
await context.read<DocumentsCubit>().loadMore();
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSelected(DocumentModel model) {
|
void _onSelected(DocumentModel model) {
|
||||||
context.read<DocumentsCubit>().toggleDocumentSelection(model);
|
context.read<DocumentsCubit>().toggleDocumentSelection(model);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
class DocumentsEmptyState extends StatelessWidget {
|
class DocumentsEmptyState extends StatelessWidget {
|
||||||
final DocumentsState state;
|
final DocumentsPagedState state;
|
||||||
final VoidCallback onReset;
|
final VoidCallback onReset;
|
||||||
const DocumentsEmptyState({
|
const DocumentsEmptyState({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|||||||
@@ -64,13 +64,6 @@ class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
isSelected: state.selectedIds.contains(document.id),
|
isSelected: state.selectedIds.contains(document.id),
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
isAtLeastOneSelected: state.selection.isNotEmpty,
|
||||||
isTagSelectedPredicate: (int tagId) {
|
|
||||||
return state.filter.tags is IdsTagsQuery
|
|
||||||
? (state.filter.tags as IdsTagsQuery)
|
|
||||||
.includedIds
|
|
||||||
.contains(tagId)
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
onTagSelected: onTagSelected,
|
onTagSelected: onTagSelected,
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
|||||||
@@ -7,31 +7,32 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d
|
|||||||
class DocumentListItem extends StatelessWidget {
|
class DocumentListItem extends StatelessWidget {
|
||||||
static const _a4AspectRatio = 1 / 1.4142;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
final void Function(DocumentModel) onTap;
|
final void Function(DocumentModel)? onTap;
|
||||||
final void Function(DocumentModel)? onSelected;
|
final void Function(DocumentModel)? onSelected;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final bool isAtLeastOneSelected;
|
final bool isAtLeastOneSelected;
|
||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
final bool Function(int tagId) isTagSelectedPredicate;
|
|
||||||
|
|
||||||
final void Function(int tagId)? onTagSelected;
|
final void Function(int tagId)? onTagSelected;
|
||||||
final void Function(int? correspondentId)? onCorrespondentSelected;
|
final void Function(int? correspondentId)? onCorrespondentSelected;
|
||||||
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
|
||||||
|
final bool enableHeroAnimation;
|
||||||
|
|
||||||
const DocumentListItem({
|
const DocumentListItem({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.document,
|
required this.document,
|
||||||
required this.onTap,
|
this.onTap,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
required this.isSelected,
|
this.isSelected = false,
|
||||||
required this.isAtLeastOneSelected,
|
this.isAtLeastOneSelected = false,
|
||||||
this.isLabelClickable = true,
|
this.isLabelClickable = true,
|
||||||
required this.isTagSelectedPredicate,
|
|
||||||
this.onTagSelected,
|
this.onTagSelected,
|
||||||
this.onCorrespondentSelected,
|
this.onCorrespondentSelected,
|
||||||
this.onDocumentTypeSelected,
|
this.onDocumentTypeSelected,
|
||||||
this.onStoragePathSelected,
|
this.onStoragePathSelected,
|
||||||
|
this.enableHeroAnimation = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -85,6 +86,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
id: document.id,
|
id: document.id,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
|
enableHero: enableHeroAnimation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -96,7 +98,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
if (isAtLeastOneSelected || isSelected) {
|
if (isAtLeastOneSelected || isSelected) {
|
||||||
onSelected?.call(document);
|
onSelected?.call(document);
|
||||||
} else {
|
} else {
|
||||||
onTap(document);
|
onTap?.call(document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
if (!state.isLoaded)
|
if (!state.isLoaded)
|
||||||
Expanded(child: const DocumentsListLoadingWidget())
|
const Expanded(child: DocumentsListLoadingWidget())
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@@ -59,10 +59,6 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isSelected: false,
|
|
||||||
isAtLeastOneSelected: false,
|
|
||||||
isTagSelectedPredicate: (_) => false,
|
|
||||||
onTagSelected: (int tag) {},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Base state for all blocs/cubits using a paged view of documents.
|
||||||
|
/// [T] is the return type of the API call.
|
||||||
|
///
|
||||||
abstract class DocumentsPagedState extends Equatable {
|
abstract class DocumentsPagedState extends Equatable {
|
||||||
final bool hasLoaded;
|
final bool hasLoaded;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
||||||
|
|
||||||
|
part 'similar_documents_state.dart';
|
||||||
|
|
||||||
|
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||||
|
with DocumentsPagingMixin<SimilarDocumentsState> {
|
||||||
|
final int documentId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
|
SimilarDocumentsCubit(
|
||||||
|
this.api, {
|
||||||
|
required this.documentId,
|
||||||
|
}) : super(const SimilarDocumentsState());
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (!state.hasLoaded) {
|
||||||
|
await updateFilter(
|
||||||
|
filter: state.filter.copyWith(moreLike: () => documentId),
|
||||||
|
);
|
||||||
|
emit(state.copyWith(hasLoaded: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
part of 'similar_documents_cubit.dart';
|
||||||
|
|
||||||
|
class SimilarDocumentsState extends DocumentsPagedState {
|
||||||
|
const SimilarDocumentsState({
|
||||||
|
super.filter,
|
||||||
|
super.hasLoaded,
|
||||||
|
super.isLoading,
|
||||||
|
super.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [
|
||||||
|
filter,
|
||||||
|
hasLoaded,
|
||||||
|
isLoading,
|
||||||
|
value,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
SimilarDocumentsState copyWithPaged({
|
||||||
|
bool? hasLoaded,
|
||||||
|
bool? isLoading,
|
||||||
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
|
DocumentFilter? filter,
|
||||||
|
}) {
|
||||||
|
return copyWith(
|
||||||
|
hasLoaded: hasLoaded,
|
||||||
|
isLoading: isLoading,
|
||||||
|
value: value,
|
||||||
|
filter: filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SimilarDocumentsState copyWith({
|
||||||
|
bool? hasLoaded,
|
||||||
|
bool? isLoading,
|
||||||
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
|
DocumentFilter? filter,
|
||||||
|
}) {
|
||||||
|
return SimilarDocumentsState(
|
||||||
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
value: value ?? this.value,
|
||||||
|
filter: filter ?? this.filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,8 @@
|
|||||||
"@documentDetailsPageTabMetaDataLabel": {},
|
"@documentDetailsPageTabMetaDataLabel": {},
|
||||||
"documentDetailsPageTabOverviewLabel": "Přehled",
|
"documentDetailsPageTabOverviewLabel": "Přehled",
|
||||||
"@documentDetailsPageTabOverviewLabel": {},
|
"@documentDetailsPageTabOverviewLabel": {},
|
||||||
|
"documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents",
|
||||||
|
"@documentDetailsPageTabSimilarDocumentsLabel": {},
|
||||||
"documentDocumentTypePropertyLabel": "Typ dokumentu",
|
"documentDocumentTypePropertyLabel": "Typ dokumentu",
|
||||||
"@documentDocumentTypePropertyLabel": {},
|
"@documentDocumentTypePropertyLabel": {},
|
||||||
"documentDownloadSuccessMessage": "Dokument úspěšně stažen.",
|
"documentDownloadSuccessMessage": "Dokument úspěšně stažen.",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
"@documentDetailsPageTabMetaDataLabel": {},
|
"@documentDetailsPageTabMetaDataLabel": {},
|
||||||
"documentDetailsPageTabOverviewLabel": "Übersicht",
|
"documentDetailsPageTabOverviewLabel": "Übersicht",
|
||||||
"@documentDetailsPageTabOverviewLabel": {},
|
"@documentDetailsPageTabOverviewLabel": {},
|
||||||
|
"documentDetailsPageTabSimilarDocumentsLabel": "Ähnliche Dokumente",
|
||||||
|
"@documentDetailsPageTabSimilarDocumentsLabel": {},
|
||||||
"documentDocumentTypePropertyLabel": "Dokumenttyp",
|
"documentDocumentTypePropertyLabel": "Dokumenttyp",
|
||||||
"@documentDocumentTypePropertyLabel": {},
|
"@documentDocumentTypePropertyLabel": {},
|
||||||
"documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.",
|
"documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
"@documentDetailsPageTabMetaDataLabel": {},
|
"@documentDetailsPageTabMetaDataLabel": {},
|
||||||
"documentDetailsPageTabOverviewLabel": "Overview",
|
"documentDetailsPageTabOverviewLabel": "Overview",
|
||||||
"@documentDetailsPageTabOverviewLabel": {},
|
"@documentDetailsPageTabOverviewLabel": {},
|
||||||
|
"documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents",
|
||||||
|
"@documentDetailsPageTabSimilarDocumentsLabel": {},
|
||||||
"documentDocumentTypePropertyLabel": "Document Type",
|
"documentDocumentTypePropertyLabel": "Document Type",
|
||||||
"@documentDocumentTypePropertyLabel": {},
|
"@documentDocumentTypePropertyLabel": {},
|
||||||
"documentDownloadSuccessMessage": "Document successfully downloaded.",
|
"documentDownloadSuccessMessage": "Document successfully downloaded.",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
"@documentDetailsPageTabMetaDataLabel": {},
|
"@documentDetailsPageTabMetaDataLabel": {},
|
||||||
"documentDetailsPageTabOverviewLabel": "Genel bakış",
|
"documentDetailsPageTabOverviewLabel": "Genel bakış",
|
||||||
"@documentDetailsPageTabOverviewLabel": {},
|
"@documentDetailsPageTabOverviewLabel": {},
|
||||||
|
"documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents",
|
||||||
|
"@documentDetailsPageTabSimilarDocumentsLabel": {},
|
||||||
"documentDocumentTypePropertyLabel": "Döküman tipi",
|
"documentDocumentTypePropertyLabel": "Döküman tipi",
|
||||||
"@documentDocumentTypePropertyLabel": {},
|
"@documentDocumentTypePropertyLabel": {},
|
||||||
"documentDownloadSuccessMessage": "Döküman başarıyla indirildi.",
|
"documentDownloadSuccessMessage": "Döküman başarıyla indirildi.",
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export 'document_model_json_converter.dart';
|
export 'document_model_json_converter.dart';
|
||||||
export 'similar_document_model_json_converter.dart';
|
|
||||||
export 'date_range_query_json_converter.dart';
|
export 'date_range_query_json_converter.dart';
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
|
|
||||||
class SimilarDocumentModelJsonConverter
|
|
||||||
extends JsonConverter<SimilarDocumentModel, Map<String, dynamic>> {
|
|
||||||
@override
|
|
||||||
SimilarDocumentModel fromJson(Map<String, dynamic> json) {
|
|
||||||
return SimilarDocumentModel.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson(SimilarDocumentModel object) {
|
|
||||||
return object.toJson();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,6 +33,9 @@ class DocumentFilter extends Equatable {
|
|||||||
final DateRangeQuery modified;
|
final DateRangeQuery modified;
|
||||||
final TextQuery query;
|
final TextQuery query;
|
||||||
|
|
||||||
|
/// Query documents similar to the document with this id.
|
||||||
|
final int? moreLike;
|
||||||
|
|
||||||
const DocumentFilter({
|
const DocumentFilter({
|
||||||
this.documentType = const IdQueryParameter.unset(),
|
this.documentType = const IdQueryParameter.unset(),
|
||||||
this.correspondent = const IdQueryParameter.unset(),
|
this.correspondent = const IdQueryParameter.unset(),
|
||||||
@@ -47,6 +50,7 @@ class DocumentFilter extends Equatable {
|
|||||||
this.added = const UnsetDateRangeQuery(),
|
this.added = const UnsetDateRangeQuery(),
|
||||||
this.created = const UnsetDateRangeQuery(),
|
this.created = const UnsetDateRangeQuery(),
|
||||||
this.modified = const UnsetDateRangeQuery(),
|
this.modified = const UnsetDateRangeQuery(),
|
||||||
|
this.moreLike,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get forceExtendedQuery {
|
bool get forceExtendedQuery {
|
||||||
@@ -77,6 +81,10 @@ class DocumentFilter extends Equatable {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (moreLike != null) {
|
||||||
|
params.add(MapEntry('more_like_id', moreLike.toString()));
|
||||||
|
}
|
||||||
// Reverse ordering can also be encoded using &reverse=1
|
// Reverse ordering can also be encoded using &reverse=1
|
||||||
// Merge query params
|
// Merge query params
|
||||||
final queryParams = groupBy(params, (e) => e.key).map(
|
final queryParams = groupBy(params, (e) => e.key).map(
|
||||||
@@ -107,7 +115,7 @@ class DocumentFilter extends Equatable {
|
|||||||
DateRangeQuery? created,
|
DateRangeQuery? created,
|
||||||
DateRangeQuery? modified,
|
DateRangeQuery? modified,
|
||||||
TextQuery? query,
|
TextQuery? query,
|
||||||
int? selectedViewId,
|
int? Function()? moreLike,
|
||||||
}) {
|
}) {
|
||||||
final newFilter = DocumentFilter(
|
final newFilter = DocumentFilter(
|
||||||
pageSize: pageSize ?? this.pageSize,
|
pageSize: pageSize ?? this.pageSize,
|
||||||
@@ -123,6 +131,7 @@ class DocumentFilter extends Equatable {
|
|||||||
added: added ?? this.added,
|
added: added ?? this.added,
|
||||||
created: created ?? this.created,
|
created: created ?? this.created,
|
||||||
modified: modified ?? this.modified,
|
modified: modified ?? this.modified,
|
||||||
|
moreLike: moreLike != null ? moreLike.call() : this.moreLike,
|
||||||
);
|
);
|
||||||
if (query?.queryType != QueryType.extended &&
|
if (query?.queryType != QueryType.extended &&
|
||||||
newFilter.forceExtendedQuery) {
|
newFilter.forceExtendedQuery) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ DocumentFilter _$DocumentFilterFromJson(Map<String, dynamic> json) =>
|
|||||||
? const UnsetDateRangeQuery()
|
? const UnsetDateRangeQuery()
|
||||||
: const DateRangeQueryJsonConverter()
|
: const DateRangeQueryJsonConverter()
|
||||||
.fromJson(json['modified'] as Map<String, dynamic>),
|
.fromJson(json['modified'] as Map<String, dynamic>),
|
||||||
|
moreLike: json['moreLike'] as int?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DocumentFilterToJson(DocumentFilter instance) =>
|
Map<String, dynamic> _$DocumentFilterToJson(DocumentFilter instance) =>
|
||||||
@@ -65,6 +66,7 @@ Map<String, dynamic> _$DocumentFilterToJson(DocumentFilter instance) =>
|
|||||||
'added': const DateRangeQueryJsonConverter().toJson(instance.added),
|
'added': const DateRangeQueryJsonConverter().toJson(instance.added),
|
||||||
'modified': const DateRangeQueryJsonConverter().toJson(instance.modified),
|
'modified': const DateRangeQueryJsonConverter().toJson(instance.modified),
|
||||||
'query': instance.query.toJson(),
|
'query': instance.query.toJson(),
|
||||||
|
'moreLike': instance.moreLike,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$SortFieldEnumMap = {
|
const _$SortFieldEnumMap = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
|
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
|
||||||
|
import 'package:paperless_api/src/models/search_hit.dart';
|
||||||
|
|
||||||
part 'document_model.g.dart';
|
part 'document_model.g.dart';
|
||||||
|
|
||||||
@@ -37,6 +38,12 @@ class DocumentModel extends Equatable {
|
|||||||
final String originalFileName;
|
final String originalFileName;
|
||||||
final String? archivedFileName;
|
final String? archivedFileName;
|
||||||
|
|
||||||
|
@JsonKey(
|
||||||
|
name: '__search_hit__',
|
||||||
|
includeIfNull: false,
|
||||||
|
)
|
||||||
|
final SearchHit? searchHit;
|
||||||
|
|
||||||
const DocumentModel({
|
const DocumentModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
@@ -51,6 +58,7 @@ class DocumentModel extends Equatable {
|
|||||||
required this.originalFileName,
|
required this.originalFileName,
|
||||||
this.archivedFileName,
|
this.archivedFileName,
|
||||||
this.storagePath,
|
this.storagePath,
|
||||||
|
this.searchHit,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory DocumentModel.fromJson(Map<String, dynamic> json) =>
|
factory DocumentModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ DocumentModel _$DocumentModelFromJson(Map<String, dynamic> json) =>
|
|||||||
originalFileName: json['original_file_name'] as String,
|
originalFileName: json['original_file_name'] as String,
|
||||||
archivedFileName: json['archived_file_name'] as String?,
|
archivedFileName: json['archived_file_name'] as String?,
|
||||||
storagePath: json['storage_path'] as int?,
|
storagePath: json['storage_path'] as int?,
|
||||||
|
searchHit: json['__search_hit__'] == null
|
||||||
|
? null
|
||||||
|
: SearchHit.fromJson(json['__search_hit__'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DocumentModelToJson(DocumentModel instance) =>
|
Map<String, dynamic> _$DocumentModelToJson(DocumentModel instance) {
|
||||||
<String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'content': instance.content,
|
'content': instance.content,
|
||||||
@@ -43,3 +46,13 @@ Map<String, dynamic> _$DocumentModelToJson(DocumentModel instance) =>
|
|||||||
'original_file_name': instance.originalFileName,
|
'original_file_name': instance.originalFileName,
|
||||||
'archived_file_name': instance.archivedFileName,
|
'archived_file_name': instance.archivedFileName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('__search_hit__', instance.searchHit);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export 'paperless_server_exception.dart';
|
|||||||
export 'paperless_server_information_model.dart';
|
export 'paperless_server_information_model.dart';
|
||||||
export 'paperless_server_statistics_model.dart';
|
export 'paperless_server_statistics_model.dart';
|
||||||
export 'saved_view_model.dart';
|
export 'saved_view_model.dart';
|
||||||
export 'similar_document_model.dart';
|
|
||||||
export 'task/task.dart';
|
export 'task/task.dart';
|
||||||
export 'task/task_status.dart';
|
export 'task/task_status.dart';
|
||||||
export 'field_suggestions.dart';
|
export 'field_suggestions.dart';
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
|
|
||||||
import 'package:paperless_api/src/models/document_model.dart';
|
|
||||||
import 'package:paperless_api/src/models/search_hit.dart';
|
|
||||||
|
|
||||||
part 'similar_document_model.g.dart';
|
|
||||||
|
|
||||||
@LocalDateTimeJsonConverter()
|
|
||||||
@JsonSerializable()
|
|
||||||
class SimilarDocumentModel extends DocumentModel {
|
|
||||||
@JsonKey(name: '__search_hit__')
|
|
||||||
final SearchHit searchHit;
|
|
||||||
|
|
||||||
const SimilarDocumentModel({
|
|
||||||
required super.id,
|
|
||||||
required super.title,
|
|
||||||
required super.documentType,
|
|
||||||
required super.correspondent,
|
|
||||||
required super.created,
|
|
||||||
required super.modified,
|
|
||||||
required super.added,
|
|
||||||
required super.originalFileName,
|
|
||||||
required this.searchHit,
|
|
||||||
super.archiveSerialNumber,
|
|
||||||
super.archivedFileName,
|
|
||||||
super.content,
|
|
||||||
super.storagePath,
|
|
||||||
super.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory SimilarDocumentModel.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SimilarDocumentModelFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$SimilarDocumentModelToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'similar_document_model.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
SimilarDocumentModel _$SimilarDocumentModelFromJson(
|
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
SimilarDocumentModel(
|
|
||||||
id: json['id'] as int,
|
|
||||||
title: json['title'] as String,
|
|
||||||
documentType: json['documentType'] as int?,
|
|
||||||
correspondent: json['correspondent'] as int?,
|
|
||||||
created: const LocalDateTimeJsonConverter()
|
|
||||||
.fromJson(json['created'] as String),
|
|
||||||
modified: const LocalDateTimeJsonConverter()
|
|
||||||
.fromJson(json['modified'] as String),
|
|
||||||
added:
|
|
||||||
const LocalDateTimeJsonConverter().fromJson(json['added'] as String),
|
|
||||||
originalFileName: json['originalFileName'] as String,
|
|
||||||
searchHit:
|
|
||||||
SearchHit.fromJson(json['__search_hit__'] as Map<String, dynamic>),
|
|
||||||
archiveSerialNumber: json['archiveSerialNumber'] as int?,
|
|
||||||
archivedFileName: json['archivedFileName'] as String?,
|
|
||||||
content: json['content'] as String?,
|
|
||||||
storagePath: json['storagePath'] as int?,
|
|
||||||
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as int) ??
|
|
||||||
const <int>[],
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SimilarDocumentModelToJson(
|
|
||||||
SimilarDocumentModel instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'title': instance.title,
|
|
||||||
'content': instance.content,
|
|
||||||
'tags': instance.tags.toList(),
|
|
||||||
'documentType': instance.documentType,
|
|
||||||
'correspondent': instance.correspondent,
|
|
||||||
'storagePath': instance.storagePath,
|
|
||||||
'created': const LocalDateTimeJsonConverter().toJson(instance.created),
|
|
||||||
'modified': const LocalDateTimeJsonConverter().toJson(instance.modified),
|
|
||||||
'added': const LocalDateTimeJsonConverter().toJson(instance.added),
|
|
||||||
'archiveSerialNumber': instance.archiveSerialNumber,
|
|
||||||
'originalFileName': instance.originalFileName,
|
|
||||||
'archivedFileName': instance.archivedFileName,
|
|
||||||
'__search_hit__': instance.searchHit,
|
|
||||||
};
|
|
||||||
@@ -18,7 +18,6 @@ abstract class PaperlessDocumentsApi {
|
|||||||
Future<int> findNextAsn();
|
Future<int> findNextAsn();
|
||||||
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
|
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
|
||||||
Future<DocumentModel?> find(int id);
|
Future<DocumentModel?> find(int id);
|
||||||
Future<List<SimilarDocumentModel>> findSimilar(int docId);
|
|
||||||
Future<int> delete(DocumentModel doc);
|
Future<int> delete(DocumentModel doc);
|
||||||
Future<DocumentMetaData> getMetaData(DocumentModel document);
|
Future<DocumentMetaData> getMetaData(DocumentModel document);
|
||||||
Future<Iterable<int>> bulkAction(BulkAction action);
|
Future<Iterable<int>> bulkAction(BulkAction action);
|
||||||
|
|||||||
@@ -241,27 +241,6 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<SimilarDocumentModel>> findSimilar(int docId) async {
|
|
||||||
try {
|
|
||||||
final response =
|
|
||||||
await client.get("/api/documents/?more_like=$docId&pageSize=10");
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return (await compute(
|
|
||||||
PagedSearchResult<SimilarDocumentModel>.fromJsonSingleParam,
|
|
||||||
PagedSearchResultJsonSerializer(
|
|
||||||
response.data,
|
|
||||||
SimilarDocumentModelJsonConverter(),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.results;
|
|
||||||
}
|
|
||||||
throw const PaperlessServerException(ErrorCode.similarQueryError);
|
|
||||||
} on DioError catch (err) {
|
|
||||||
throw err.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FieldSuggestions> findSuggestions(DocumentModel document) async {
|
Future<FieldSuggestions> findSuggestions(DocumentModel document) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user