mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 06:07:57 -06:00
Initial commit
This commit is contained in:
418
lib/features/documents/view/pages/document_details_page.dart
Normal file
418
lib/features/documents/view/pages/document_details_page.dart
Normal file
@@ -0,0 +1,418 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_bloc_provider.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/core/widgets/highlighted_text.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_meta_data.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/repository/document_repository.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/pages/document_edit_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class DocumentDetailsPage extends StatefulWidget {
|
||||
final int documentId;
|
||||
const DocumentDetailsPage({
|
||||
Key? key,
|
||||
required this.documentId,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DocumentDetailsPage> createState() => _DocumentDetailsPageState();
|
||||
}
|
||||
|
||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
static final DateFormat _detailedDateFormat = DateFormat("MMM d, yyyy HH:mm:ss");
|
||||
|
||||
bool _isDownloadPending = false;
|
||||
bool _isAssignAsnPending = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
// buildWhen required because rebuild would happen after delete causing error.
|
||||
buildWhen: (previous, current) {
|
||||
return current.documents.where((element) => element.id == widget.documentId).isNotEmpty;
|
||||
},
|
||||
builder: (context, state) {
|
||||
final document = state.documents.where((doc) => doc.id == widget.documentId).first;
|
||||
return SafeArea(
|
||||
bottom: true,
|
||||
child: DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.edit),
|
||||
onPressed: () => _onEdit(document),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(document),
|
||||
).padded(const EdgeInsets.symmetric(horizontal: 8.0)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: null, //() => _onDownload(document), //TODO: FIX
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
onPressed: () => _onOpen(document),
|
||||
).padded(const EdgeInsets.symmetric(horizontal: 8.0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors
|
||||
.black, //TODO: check if there is a way to dynamically determine color...
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
floating: true,
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
flexibleSpace: DocumentPreview(
|
||||
id: document.id,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
bottom: ColoredTabBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
tabBar: TabBar(
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context).documentDetailsPageTabOverviewLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context).documentDetailsPageTabContentLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context).documentDetailsPageTabMetaDataLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_buildDocumentOverview(document, state.filter.titleAndContentMatchString),
|
||||
_buildDocumentContentView(document, state.filter.titleAndContentMatchString),
|
||||
_buildDocumentMetaDataView(document),
|
||||
].padded(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentMetaDataView(DocumentModel document) {
|
||||
return FutureBuilder<DocumentMetaData>(
|
||||
future: getIt<DocumentRepository>().getMetaData(document),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final meta = snapshot.data!;
|
||||
return ListView(
|
||||
children: [
|
||||
_DetailsItem.text(_detailedDateFormat.format(document.modified),
|
||||
label: S.of(context).documentModifiedPropertyLabel, context: context),
|
||||
_separator(),
|
||||
_DetailsItem.text(_detailedDateFormat.format(document.added),
|
||||
label: S.of(context).documentAddedPropertyLabel, context: context),
|
||||
_separator(),
|
||||
_DetailsItem(
|
||||
label: S.of(context).documentArchiveSerialNumberPropertyLongLabel,
|
||||
content: document.archiveSerialNumber != null
|
||||
? Text(document.archiveSerialNumber.toString())
|
||||
: OutlinedButton(
|
||||
child: Text(S.of(context).documentDetailsPageAssignAsnButtonLabel),
|
||||
onPressed: () => BlocProvider.of<DocumentsCubit>(context).assignAsn(document),
|
||||
),
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem.text(
|
||||
meta.mediaFilename,
|
||||
context: context,
|
||||
label: S.of(context).documentMetaDataMediaFilenamePropertyLabel,
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem.text(
|
||||
meta.originalChecksum,
|
||||
context: context,
|
||||
label: S.of(context).documentMetaDataChecksumLabel,
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem.text(formatBytes(meta.originalSize, 2),
|
||||
label: S.of(context).documentMetaDataOriginalFileSizeLabel, context: context),
|
||||
_separator(),
|
||||
_DetailsItem.text(
|
||||
meta.originalMimeType,
|
||||
label: S.of(context).documentMetaDataOriginalMimeTypeLabel,
|
||||
context: context,
|
||||
),
|
||||
_separator(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentContentView(DocumentModel document, String? match) {
|
||||
return SingleChildScrollView(
|
||||
child: _DetailsItem(
|
||||
content: HighlightedText(
|
||||
text: document.content ?? "",
|
||||
highlights: match == null ? [] : match.split(" "),
|
||||
style: Theme.of(context).textTheme.bodyText2,
|
||||
caseSensitive: false,
|
||||
),
|
||||
label: S.of(context).documentDetailsPageTabContentLabel,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentOverview(DocumentModel document, String? match) {
|
||||
return ListView(
|
||||
children: [
|
||||
_DetailsItem(
|
||||
content: HighlightedText(
|
||||
text: document.title,
|
||||
highlights: match?.split(" ") ?? <String>[],
|
||||
),
|
||||
label: S.of(context).documentTitlePropertyLabel,
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem.text(
|
||||
DateFormat.yMMMd(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(document.created),
|
||||
context: context,
|
||||
label: S.of(context).documentCreatedPropertyLabel,
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem(
|
||||
content: DocumentTypeWidget(
|
||||
documentTypeId: document.documentType,
|
||||
afterSelected: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem(
|
||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||
content: CorrespondentWidget(
|
||||
correspondentId: document.correspondent,
|
||||
afterSelected: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem(
|
||||
label: S.of(context).documentStoragePathPropertyLabel,
|
||||
content: StoragePathWidget(
|
||||
pathId: document.storagePath,
|
||||
afterSelected: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
_separator(),
|
||||
_DetailsItem(
|
||||
label: S.of(context).documentTagsPropertyLabel,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TagsWidget(
|
||||
tagIds: document.tags,
|
||||
),
|
||||
),
|
||||
),
|
||||
// _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(),
|
||||
// );
|
||||
// }),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _separator() {
|
||||
return const SizedBox(height: 32.0);
|
||||
}
|
||||
|
||||
void _onEdit(DocumentModel document) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LabelBlocProvider(
|
||||
child: DocumentEditPage(document: document),
|
||||
),
|
||||
maintainState: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDownload(DocumentModel document) async {
|
||||
setState(() {
|
||||
_isDownloadPending = true;
|
||||
});
|
||||
getIt<DocumentRepository>().download(document).then((bytes) async {
|
||||
//FIXME: logic currently flawed, some error somewhere but cannot look into directory...
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dirPath = dir.path + "/files/";
|
||||
var filePath = dirPath + document.originalFileName;
|
||||
|
||||
if (File(filePath).existsSync()) {
|
||||
final count = dir
|
||||
.listSync()
|
||||
.where((entity) => (entity.path.contains(document.originalFileName)))
|
||||
.fold<int>(0, (previous, element) => previous + 1);
|
||||
final extSeperationIdx = filePath.lastIndexOf(".");
|
||||
filePath =
|
||||
filePath.replaceRange(extSeperationIdx, extSeperationIdx + 1, " (${count + 1}).");
|
||||
}
|
||||
Directory(dirPath).createSync();
|
||||
await File(filePath).writeAsBytes(bytes);
|
||||
_isDownloadPending = false;
|
||||
showSnackBar(context, "Document successfully downloaded to $filePath"); //TODO: INTL
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onDelete(DocumentModel document) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteDocumentConfirmationDialog(document: document)).then((delete) {
|
||||
if (delete ?? false) {
|
||||
BlocProvider.of<DocumentsCubit>(context).removeDocument(document).then((value) {
|
||||
Navigator.pop(context);
|
||||
showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
|
||||
}).onError<ErrorMessage>((error, _) {
|
||||
showSnackBar(context, translateError(context, error.code));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onOpen(DocumentModel document) async {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DocumentView(document: document),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String formatBytes(int bytes, int decimals) {
|
||||
if (bytes <= 0) return "0 B";
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
var i = (log(bytes) / log(1024)).floor();
|
||||
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i];
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailsItem extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget content;
|
||||
const _DetailsItem({Key? key, required this.label, required this.content}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_DetailsItem.text(
|
||||
String text, {
|
||||
required this.label,
|
||||
required BuildContext context,
|
||||
}) : content = Text(text, style: Theme.of(context).textTheme.bodyText2);
|
||||
}
|
||||
|
||||
class ColoredTabBar extends Container implements PreferredSizeWidget {
|
||||
ColoredTabBar({
|
||||
super.key,
|
||||
required this.backgroundColor,
|
||||
required this.tabBar,
|
||||
});
|
||||
|
||||
final TabBar tabBar;
|
||||
final Color backgroundColor;
|
||||
@override
|
||||
Size get preferredSize => tabBar.preferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: backgroundColor,
|
||||
child: tabBar,
|
||||
);
|
||||
}
|
||||
204
lib/features/documents/view/pages/document_edit_page.dart
Normal file
204
lib/features/documents/view/pages/document_edit_page.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/ids_query_parameter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/repository/document_repository.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DocumentEditPage extends StatefulWidget {
|
||||
final DocumentModel document;
|
||||
const DocumentEditPage({Key? key, required this.document}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DocumentEditPage> createState() => _DocumentEditPageState();
|
||||
}
|
||||
|
||||
class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
static const fkTitle = "title";
|
||||
static const fkCorrespondent = "correspondent";
|
||||
static const fkTags = "tags";
|
||||
static const fkDocumentType = "documentType";
|
||||
static const fkCreatedDate = "createdAtDate";
|
||||
static const fkStoragePath = 'storagePath';
|
||||
|
||||
late Future<Uint8List> documentBytes;
|
||||
|
||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||
bool _isSubmitLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
documentBytes = getIt<DocumentRepository>().getPreview(widget.document.id);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final values = _formKey.currentState!.value;
|
||||
final updatedDocument = widget.document.copyWith(
|
||||
title: values[fkTitle],
|
||||
created: values[fkCreatedDate],
|
||||
documentType: values[fkDocumentType] as IdQueryParameter,
|
||||
correspondent: values[fkCorrespondent] as IdQueryParameter,
|
||||
storagePath: values[fkStoragePath] as IdQueryParameter,
|
||||
tags: values[fkTags] as IdsQueryParameter,
|
||||
);
|
||||
setState(() {
|
||||
_isSubmitLoading = true;
|
||||
});
|
||||
await getIt<DocumentsCubit>().updateDocument(updatedDocument);
|
||||
Navigator.pop(context);
|
||||
showSnackBar(context, "Document successfully updated."); //TODO: INTL
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.save),
|
||||
label: Text(S.of(context).genericActionSaveLabel),
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).documentEditPageTitle),
|
||||
bottom: _isSubmitLoading
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(4),
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
extendBody: true,
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
top: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(children: [
|
||||
_buildTitleFormField().padded(),
|
||||
_buildCreatedAtFormField().padded(),
|
||||
BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<DocumentType, DocumentTypeQuery>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (currentInput) => BlocProvider.value(
|
||||
value: BlocProvider.of<DocumentTypeCubit>(context),
|
||||
child: AddDocumentTypePage(
|
||||
initialName: currentInput,
|
||||
),
|
||||
),
|
||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||
initialValue: DocumentTypeQuery.fromId(widget.document.documentType),
|
||||
state: state,
|
||||
name: fkDocumentType,
|
||||
queryParameterIdBuilder: DocumentTypeQuery.fromId,
|
||||
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
);
|
||||
},
|
||||
).padded(),
|
||||
BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<Correspondent, CorrespondentQuery>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
|
||||
value: BlocProvider.of<CorrespondentCubit>(context),
|
||||
child: AddCorrespondentPage(initalValue: initialValue),
|
||||
),
|
||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||
state: state,
|
||||
initialValue: CorrespondentQuery.fromId(widget.document.correspondent),
|
||||
name: fkCorrespondent,
|
||||
queryParameterIdBuilder: CorrespondentQuery.fromId,
|
||||
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
);
|
||||
},
|
||||
).padded(),
|
||||
BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<StoragePath, StoragePathQuery>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
|
||||
value: BlocProvider.of<StoragePathCubit>(context),
|
||||
child: AddStoragePathPage(initalValue: initialValue),
|
||||
),
|
||||
label: S.of(context).documentStoragePathPropertyLabel,
|
||||
state: state,
|
||||
initialValue: StoragePathQuery.fromId(widget.document.storagePath),
|
||||
name: fkStoragePath,
|
||||
queryParameterIdBuilder: StoragePathQuery.fromId,
|
||||
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
);
|
||||
},
|
||||
).padded(),
|
||||
TagFormField(
|
||||
initialValue: TagsQuery.fromIds(widget.document.tags),
|
||||
name: fkTags,
|
||||
).padded(),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitleFormField() {
|
||||
return FormBuilderTextField(
|
||||
name: fkTitle,
|
||||
validator: FormBuilderValidators.required(),
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context).documentTitlePropertyLabel),
|
||||
),
|
||||
initialValue: widget.document.title,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreatedAtFormField() {
|
||||
return FormBuilderDateTimePicker(
|
||||
inputType: InputType.date,
|
||||
name: fkCreatedDate,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||
),
|
||||
initialValue: widget.document.created,
|
||||
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
|
||||
initialEntryMode: DatePickerEntryMode.calendar,
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/features/documents/view/pages/document_view.dart
Normal file
50
lib/features/documents/view/pages/document_view.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/repository/document_repository.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:pdfx/pdfx.dart';
|
||||
|
||||
class DocumentView extends StatefulWidget {
|
||||
final DocumentModel document;
|
||||
|
||||
const DocumentView({
|
||||
Key? key,
|
||||
required this.document,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DocumentView> createState() => _DocumentViewState();
|
||||
}
|
||||
|
||||
class _DocumentViewState extends State<DocumentView> {
|
||||
late PdfController _pdfController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pdfController = PdfController(
|
||||
document: PdfDocument.openData(
|
||||
getIt<DocumentRepository>().getPreview(widget.document.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).documentPreviewPageTitle),
|
||||
),
|
||||
body: PdfView(
|
||||
builders: PdfViewBuilders<DefaultBuilderOptions>(
|
||||
options: const DefaultBuilderOptions(),
|
||||
pageLoaderBuilder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
controller: _pdfController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
239
lib/features/documents/view/pages/documents_page.dart
Normal file
239
lib/features/documents/view/pages/documents_page.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/core/widgets/offline_banner.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/pages/document_details_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/grid/document_grid.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/list/document_list.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||
import 'package:flutter_paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
class DocumentsPage extends StatefulWidget {
|
||||
const DocumentsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||
}
|
||||
|
||||
class _DocumentsPageState extends State<DocumentsPage> {
|
||||
final PagingController<int, DocumentModel> _pagingController =
|
||||
PagingController<int, DocumentModel>(
|
||||
firstPageKey: 1,
|
||||
);
|
||||
|
||||
final PanelController _panelController = PanelController();
|
||||
ViewType _viewType = ViewType.list;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
if (!documentsCubit.state.isLoaded) {
|
||||
documentsCubit.loadDocuments().onError<ErrorMessage>(
|
||||
(error, stackTrace) => showSnackBar(
|
||||
context,
|
||||
translateError(context, error.code),
|
||||
),
|
||||
);
|
||||
}
|
||||
_pagingController.addPageRequestListener(_loadNewPage);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadNewPage(int pageKey) async {
|
||||
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
final pageCount =
|
||||
documentsCubit.state.inferPageCount(pageSize: documentsCubit.state.filter.pageSize);
|
||||
if (pageCount <= pageKey + 1) {
|
||||
_pagingController.nextPageKey = null;
|
||||
}
|
||||
documentsCubit.loadMore();
|
||||
}
|
||||
|
||||
void _onSelected(DocumentModel model) {
|
||||
BlocProvider.of<DocumentsCubit>(context).toggleDocumentSelection(model);
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() {
|
||||
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
return documentsCubit
|
||||
.updateFilter(filter: documentsCubit.state.filter.copyWith(page: 1))
|
||||
.onError<ErrorMessage>((error, _) {
|
||||
showSnackBar(context, translateError(context, error.code));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (_panelController.isPanelOpen) {
|
||||
FocusScope.of(context).unfocus();
|
||||
_panelController.close();
|
||||
return false;
|
||||
}
|
||||
final docBloc = BlocProvider.of<DocumentsCubit>(context);
|
||||
if (docBloc.state.selection.isNotEmpty) {
|
||||
docBloc.resetSelection();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous != ConnectivityState.connected && current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
BlocProvider.of<DocumentsCubit>(context).loadDocuments();
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
return Scaffold(
|
||||
drawer: BlocProvider.value(
|
||||
value: BlocProvider.of<AuthenticationCubit>(context),
|
||||
child: const InfoDrawer(),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: connectivityState == ConnectivityState.connected ? null : const OfflineBanner(),
|
||||
body: SlidingUpPanel(
|
||||
backdropEnabled: true,
|
||||
parallaxEnabled: true,
|
||||
parallaxOffset: .5,
|
||||
controller: _panelController,
|
||||
defaultPanelState: PanelState.CLOSED,
|
||||
minHeight: 48,
|
||||
maxHeight: MediaQuery.of(context).size.height -
|
||||
kBottomNavigationBarHeight -
|
||||
2 * kToolbarHeight,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
body: _buildBody(connectivityState),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
panelBuilder: (scrollController) => DocumentFilterPanel(
|
||||
panelController: _panelController,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(ConnectivityState connectivityState) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
// Some ugly tricks to make it work with bloc, update pageController
|
||||
_pagingController.value = PagingState(
|
||||
itemList: state.documents,
|
||||
nextPageKey: state.nextPageNumber,
|
||||
);
|
||||
|
||||
late Widget child;
|
||||
switch (_viewType) {
|
||||
case ViewType.list:
|
||||
child = DocumentListView(
|
||||
onTap: _openDocumentDetails,
|
||||
state: state,
|
||||
onSelected: _onSelected,
|
||||
pagingController: _pagingController,
|
||||
hasInternetConnection: connectivityState == ConnectivityState.connected,
|
||||
);
|
||||
break;
|
||||
case ViewType.grid:
|
||||
child = DocumentGridView(
|
||||
onTap: _openDocumentDetails,
|
||||
state: state,
|
||||
onSelected: _onSelected,
|
||||
pagingController: _pagingController,
|
||||
hasInternetConnection: connectivityState == ConnectivityState.connected);
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.isLoaded && state.documents.isEmpty) {
|
||||
child = SliverToBoxAdapter(
|
||||
child: DocumentsEmptyState(
|
||||
state: state,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 142,
|
||||
), // Prevents panel from hiding scrollable content
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
DocumentsPageAppBar(
|
||||
actions: [
|
||||
const SortDocumentsButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_viewType == ViewType.grid ? Icons.list : Icons.grid_view,
|
||||
),
|
||||
onPressed: () => setState(() => _viewType = _viewType.toggle()),
|
||||
),
|
||||
],
|
||||
),
|
||||
child
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _openDocumentDetails(DocumentModel model) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||
BlocProvider.value(value: getIt<CorrespondentCubit>()),
|
||||
BlocProvider.value(value: getIt<DocumentTypeCubit>()),
|
||||
BlocProvider.value(value: getIt<TagCubit>()),
|
||||
BlocProvider.value(value: getIt<StoragePathCubit>()),
|
||||
],
|
||||
child: DocumentDetailsPage(
|
||||
documentId: model.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewType {
|
||||
grid,
|
||||
list;
|
||||
|
||||
ViewType toggle() {
|
||||
return this == grid ? list : grid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
||||
final DocumentModel document;
|
||||
const DeleteDocumentConfirmationDialog({super.key, required this.document});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).documentsPageSelectionBulkDeleteDialogTitle),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).documentsPageSelectionBulkDeleteDialogWarningTextOne,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
document.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(S.of(context).genericActionCancelLabel),
|
||||
),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(S.of(context).genericActionDeleteLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
45
lib/features/documents/view/widgets/document_preview.dart
Normal file
45
lib/features/documents/view/widgets/document_preview.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/repository/document_repository.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentPreview extends StatelessWidget {
|
||||
final int id;
|
||||
final BoxFit fit;
|
||||
final Alignment alignment;
|
||||
final double borderRadius;
|
||||
|
||||
const DocumentPreview({
|
||||
Key? key,
|
||||
required this.id,
|
||||
this.fit = BoxFit.cover,
|
||||
this.alignment = Alignment.center,
|
||||
this.borderRadius = 8.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return
|
||||
// Hero(
|
||||
// tag: "document_$id",child:
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: CachedNetworkImage(
|
||||
fit: fit,
|
||||
alignment: Alignment.topCenter,
|
||||
cacheKey: "thumb_$id",
|
||||
imageUrl: getIt<DocumentRepository>().getThumbnailUrl(id),
|
||||
errorWidget: (ctxt, msg, __) => Text(msg),
|
||||
placeholder: (context, value) => Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: const SizedBox(height: 100, width: 100),
|
||||
),
|
||||
cacheManager: getIt<CacheManager>(),
|
||||
),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/widgets/empty_state.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class DocumentsEmptyState extends StatelessWidget {
|
||||
final DocumentsState state;
|
||||
const DocumentsEmptyState({
|
||||
Key? key,
|
||||
required this.state,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: EmptyState(
|
||||
title: S.of(context).documentsPageEmptyStateOopsText,
|
||||
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
|
||||
bottomChild: state.filter != DocumentFilter.initial
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
await BlocProvider.of<DocumentsCubit>(context).updateFilter();
|
||||
BlocProvider.of<SavedViewCubit>(context).resetSelection();
|
||||
},
|
||||
child: Text(
|
||||
S.of(context).documentsFilterPageResetFilterLabel,
|
||||
),
|
||||
).padded()
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/features/documents/view/widgets/grid/document_grid.dart
Normal file
48
lib/features/documents/view/widgets/grid/document_grid.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
class DocumentGridView extends StatelessWidget {
|
||||
final void Function(DocumentModel model) onTap;
|
||||
final void Function(DocumentModel) onSelected;
|
||||
final PagingController<int, DocumentModel> pagingController;
|
||||
final DocumentsState state;
|
||||
final bool hasInternetConnection;
|
||||
|
||||
const DocumentGridView({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.pagingController,
|
||||
required this.state,
|
||||
required this.onSelected,
|
||||
required this.hasInternetConnection,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PagedSliverGrid<int, DocumentModel>(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: 1 / 2,
|
||||
),
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (context, item, index) {
|
||||
return DocumentGridItem(
|
||||
document: item,
|
||||
onTap: onTap,
|
||||
isSelected: state.selection.contains(item),
|
||||
onSelected: onSelected,
|
||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
||||
);
|
||||
},
|
||||
noItemsFoundIndicatorBuilder: (context) =>
|
||||
const DocumentsListLoadingWidget(), //TODO: Replace with grid loading widget
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DocumentGridItem extends StatelessWidget {
|
||||
final DocumentModel document;
|
||||
final bool isSelected;
|
||||
final void Function(DocumentModel) onTap;
|
||||
final void Function(DocumentModel) onSelected;
|
||||
final bool isAtLeastOneSelected;
|
||||
|
||||
const DocumentGridItem({
|
||||
Key? key,
|
||||
required this.document,
|
||||
required this.onTap,
|
||||
required this.onSelected,
|
||||
required this.isSelected,
|
||||
required this.isAtLeastOneSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onLongPress: () => onSelected(document),
|
||||
child: AbsorbPointer(
|
||||
absorbing: isAtLeastOneSelected,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 1.0,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
borderRadius: 12.0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CorrespondentWidget(correspondentId: document.correspondent),
|
||||
DocumentTypeWidget(documentTypeId: document.documentType),
|
||||
Text(
|
||||
document.title,
|
||||
maxLines: document.tags.isEmpty ? 3 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
TagsWidget(
|
||||
tagIds: document.tags,
|
||||
isMultiLine: false,
|
||||
),
|
||||
Text(DateFormat.yMMMd(Intl.getCurrentLocale()).format(document.created)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (isAtLeastOneSelected || isSelected) {
|
||||
onSelected(document);
|
||||
} else {
|
||||
onTap(document);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
lib/features/documents/view/widgets/list/document_list.dart
Normal file
44
lib/features/documents/view/widgets/list/document_list.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/core/widgets/offline_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
class DocumentListView extends StatelessWidget {
|
||||
final void Function(DocumentModel model) onTap;
|
||||
final void Function(DocumentModel) onSelected;
|
||||
final PagingController<int, DocumentModel> pagingController;
|
||||
final DocumentsState state;
|
||||
final bool hasInternetConnection;
|
||||
|
||||
const DocumentListView({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.pagingController,
|
||||
required this.state,
|
||||
required this.onSelected,
|
||||
required this.hasInternetConnection,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PagedSliverList<int, DocumentModel>(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
animateTransitions: true,
|
||||
itemBuilder: (context, item, index) {
|
||||
return DocumentListItem(
|
||||
document: item,
|
||||
onTap: onTap,
|
||||
isSelected: state.selection.contains(item),
|
||||
onSelected: onSelected,
|
||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
||||
);
|
||||
},
|
||||
noItemsFoundIndicatorBuilder: (context) =>
|
||||
hasInternetConnection ? const DocumentsListLoadingWidget() : const OfflineWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||
|
||||
class DocumentListItem extends StatelessWidget {
|
||||
static const a4AspectRatio = 1 / 1.4142;
|
||||
final DocumentModel document;
|
||||
final bool isSelected;
|
||||
final void Function(DocumentModel) onTap;
|
||||
final void Function(DocumentModel)? onSelected;
|
||||
final bool isAtLeastOneSelected;
|
||||
|
||||
const DocumentListItem({
|
||||
Key? key,
|
||||
required this.document,
|
||||
required this.onTap,
|
||||
this.onSelected,
|
||||
required this.isSelected,
|
||||
required this.isAtLeastOneSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
selected: isSelected,
|
||||
onTap: () => _onTap(),
|
||||
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
onLongPress: () => onSelected?.call(document),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: isAtLeastOneSelected,
|
||||
child: CorrespondentWidget(
|
||||
correspondentId: document.correspondent,
|
||||
afterSelected: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
document.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: document.tags.isEmpty ? 2 : 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: AbsorbPointer(
|
||||
absorbing: isAtLeastOneSelected,
|
||||
child: TagsWidget(
|
||||
tagIds: document.tags,
|
||||
isMultiLine: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
isThreeLine: document.tags.isNotEmpty,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: a4AspectRatio,
|
||||
child: GestureDetector(
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (isAtLeastOneSelected || isSelected) {
|
||||
onSelected?.call(document);
|
||||
} else {
|
||||
onTap(document);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
lib/features/documents/view/widgets/order_by_dropdown.dart
Normal file
21
lib/features/documents/view/widgets/order_by_dropdown.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
|
||||
|
||||
class OrderByDropdown extends StatefulWidget {
|
||||
static const fkOrderBy = "orderBy";
|
||||
const OrderByDropdown({super.key});
|
||||
|
||||
@override
|
||||
State<OrderByDropdown> createState() => _OrderByDropdownState();
|
||||
}
|
||||
|
||||
class _OrderByDropdownState extends State<OrderByDropdown> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderDropdown<SortField>(
|
||||
name: OrderByDropdown.fkOrderBy,
|
||||
items: const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/query_type.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/scan/view/document_upload_page.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
enum DateRangeSelection { before, after }
|
||||
|
||||
class DocumentFilterPanel extends StatefulWidget {
|
||||
final PanelController panelController;
|
||||
final ScrollController scrollController;
|
||||
|
||||
const DocumentFilterPanel({
|
||||
Key? key,
|
||||
required this.panelController,
|
||||
required this.scrollController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DocumentFilterPanel> createState() => _DocumentFilterPanelState();
|
||||
}
|
||||
|
||||
class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||
static const fkCorrespondent = DocumentModel.correspondentKey;
|
||||
static const fkDocumentType = DocumentModel.documentTypeKey;
|
||||
static const fkStoragePath = DocumentModel.storagePathKey;
|
||||
static const fkQuery = "query";
|
||||
static const fkCreatedAt = DocumentModel.createdKey;
|
||||
static const fkAddedAt = DocumentModel.addedKey;
|
||||
|
||||
static const _sortFields = [
|
||||
SortField.created,
|
||||
SortField.added,
|
||||
SortField.modified,
|
||||
SortField.title,
|
||||
SortField.correspondentName,
|
||||
SortField.documentType,
|
||||
SortField.archiveSerialNumber
|
||||
];
|
||||
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
bool _isQueryLoading = false;
|
||||
|
||||
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
|
||||
if (start == null && end == null) {
|
||||
return null;
|
||||
}
|
||||
if (start != null && end != null) {
|
||||
return DateTimeRange(start: start, end: end);
|
||||
}
|
||||
assert(start != null || end != null);
|
||||
final singleDate = (start ?? end)!;
|
||||
return DateTimeRange(start: singleDate, end: singleDate);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DocumentsCubit, DocumentsState>(
|
||||
listener: (context, state) {
|
||||
// Set initial values, otherwise they would not automatically update.
|
||||
_patchFromFilter(state.filter);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_buildDragLine(),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(S.of(context).documentsFilterPageResetFilterLabel),
|
||||
onPressed: () => _resetFilter(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).documentsFilterPageTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _onApplyFilter,
|
||||
child: Text(S.of(context).documentsFilterPageApplyFilterLabel),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: widget.scrollController,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(S.of(context).documentsFilterPageSearchLabel),
|
||||
).padded(),
|
||||
_buildQueryFormField(state),
|
||||
_buildSortByChipsList(context, state),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(S.of(context).documentsFilterPageAdvancedLabel),
|
||||
).padded(),
|
||||
_buildCreatedDateRangePickerFormField(state).padded(),
|
||||
_buildAddedDateRangePickerFormField(state).padded(),
|
||||
_buildCorrespondentFormField(state).padded(),
|
||||
_buildDocumentTypeFormField(state).padded(),
|
||||
_buildStoragePathFormField(state).padded(),
|
||||
TagFormField(
|
||||
name: DocumentModel.tagsKey,
|
||||
initialValue: state.filter.tags,
|
||||
).padded(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _resetFilter(BuildContext context) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
await BlocProvider.of<DocumentsCubit>(context).updateFilter();
|
||||
BlocProvider.of<SavedViewCubit>(context).resetSelection();
|
||||
if (!widget.panelController.isPanelClosed) {
|
||||
widget.panelController.close();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDocumentTypeFormField(DocumentsState docState) {
|
||||
return BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<DocumentType, DocumentTypeQuery>(
|
||||
formBuilderState: _formKey.currentState,
|
||||
name: fkDocumentType,
|
||||
state: state,
|
||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||
initialValue: docState.filter.documentType,
|
||||
queryParameterIdBuilder: DocumentTypeQuery.fromId,
|
||||
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStoragePathFormField(DocumentsState docState) {
|
||||
return BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<StoragePath, StoragePathQuery>(
|
||||
formBuilderState: _formKey.currentState,
|
||||
name: fkStoragePath,
|
||||
state: state,
|
||||
label: S.of(context).documentStoragePathPropertyLabel,
|
||||
initialValue: docState.filter.storagePath,
|
||||
queryParameterIdBuilder: StoragePathQuery.fromId,
|
||||
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueryFormField(DocumentsState state) {
|
||||
final queryType = _formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
|
||||
QueryType.titleAndContent;
|
||||
late String label;
|
||||
switch (queryType) {
|
||||
case QueryType.title:
|
||||
label = S.of(context).documentsFilterPageQueryOptionsTitleLabel;
|
||||
break;
|
||||
case QueryType.titleAndContent:
|
||||
label = S.of(context).documentsFilterPageQueryOptionsTitleAndContentLabel;
|
||||
break;
|
||||
case QueryType.extended:
|
||||
label = S.of(context).documentsFilterPageQueryOptionsExtendedLabel;
|
||||
break;
|
||||
}
|
||||
|
||||
return FormBuilderTextField(
|
||||
name: fkQuery,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
labelText: label,
|
||||
suffixIcon: QueryTypeFormField(
|
||||
initialValue: state.filter.queryType,
|
||||
afterSelected: (queryType) => setState(() {}),
|
||||
),
|
||||
),
|
||||
initialValue: state.filter.queryText,
|
||||
).padded();
|
||||
}
|
||||
|
||||
Widget _buildDateRangePickerHelper(DocumentsState state, String formFieldKey) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
ActionChip(
|
||||
label: Text(
|
||||
S.of(context).documentsFilterPageDateRangeLastSevenDaysLabel,
|
||||
),
|
||||
onPressed: () {
|
||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||
DateTimeRange(
|
||||
start: DateUtils.addDaysToDate(DateTime.now(), -7),
|
||||
end: DateTime.now(),
|
||||
),
|
||||
);
|
||||
},
|
||||
).padded(const EdgeInsets.only(right: 8.0)),
|
||||
ActionChip(
|
||||
label: Text(
|
||||
S.of(context).documentsFilterPageDateRangeLastMonthLabel,
|
||||
),
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
final firstDayOfLastMonth = DateUtils.addMonthsToMonthDate(now, -1);
|
||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||
DateTimeRange(
|
||||
start: DateTime(firstDayOfLastMonth.year, firstDayOfLastMonth.month, now.day),
|
||||
end: DateTime.now(),
|
||||
),
|
||||
);
|
||||
},
|
||||
).padded(const EdgeInsets.only(right: 8.0)),
|
||||
ActionChip(
|
||||
label: Text(
|
||||
S.of(context).documentsFilterPageDateRangeLastThreeMonthsLabel,
|
||||
),
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
final firstDayOfLastMonth = DateUtils.addMonthsToMonthDate(now, -3);
|
||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||
DateTimeRange(
|
||||
start: DateTime(
|
||||
firstDayOfLastMonth.year,
|
||||
firstDayOfLastMonth.month,
|
||||
now.day,
|
||||
),
|
||||
end: DateTime.now(),
|
||||
),
|
||||
);
|
||||
},
|
||||
).padded(const EdgeInsets.only(right: 8.0)),
|
||||
ActionChip(
|
||||
label: Text(
|
||||
S.of(context).documentsFilterPageDateRangeLastYearLabel,
|
||||
),
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
final firstDayOfLastMonth = DateUtils.addMonthsToMonthDate(now, -12);
|
||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||
DateTimeRange(
|
||||
start: DateTime(
|
||||
firstDayOfLastMonth.year,
|
||||
firstDayOfLastMonth.month,
|
||||
now.day,
|
||||
),
|
||||
end: DateTime.now(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCorrespondentFormField(DocumentsState docState) {
|
||||
return BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<Correspondent, CorrespondentQuery>(
|
||||
formBuilderState: _formKey.currentState,
|
||||
name: fkCorrespondent,
|
||||
state: state,
|
||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||
initialValue: docState.filter.correspondent,
|
||||
queryParameterIdBuilder: CorrespondentQuery.fromId,
|
||||
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreatedDateRangePickerFormField(DocumentsState state) {
|
||||
return Column(
|
||||
children: [
|
||||
FormBuilderDateRangePicker(
|
||||
initialValue: _dateTimeRangeOfNullable(
|
||||
state.filter.createdDateAfter,
|
||||
state.filter.createdDateBefore,
|
||||
),
|
||||
pickerBuilder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
primaryColor: Theme.of(context).primaryColor,
|
||||
colorScheme: Theme.of(context).colorScheme,
|
||||
buttonTheme: Theme.of(context).buttonTheme,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
|
||||
fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel,
|
||||
fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel,
|
||||
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
|
||||
lastDate: DateTime.now(),
|
||||
name: fkCreatedAt,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () => _formKey.currentState?.fields[fkCreatedAt]?.didChange(null),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDateRangePickerHelper(state, fkCreatedAt),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddedDateRangePickerFormField(DocumentsState state) {
|
||||
return Column(
|
||||
children: [
|
||||
FormBuilderDateRangePicker(
|
||||
initialValue: _dateTimeRangeOfNullable(
|
||||
state.filter.addedDateAfter,
|
||||
state.filter.addedDateBefore,
|
||||
),
|
||||
pickerBuilder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
primaryColor: Theme.of(context).primaryColor,
|
||||
colorScheme: Theme.of(context).colorScheme,
|
||||
buttonTheme: Theme.of(context).buttonTheme,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
|
||||
fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel,
|
||||
fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel,
|
||||
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
|
||||
lastDate: DateTime.now(),
|
||||
name: fkAddedAt,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
labelText: S.of(context).documentAddedPropertyLabel,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () => _formKey.currentState?.fields[fkAddedAt]?.didChange(null),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDateRangePickerHelper(state, fkAddedAt),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDragLine() {
|
||||
return Container(
|
||||
width: 48,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSortByChipsList(BuildContext context, DocumentsState state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).documentsPageOrderByLabel,
|
||||
),
|
||||
SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: ListView.separated(
|
||||
itemCount: _sortFields.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
itemBuilder: (context, index) =>
|
||||
_buildActionChip(_sortFields[index], state.filter.sortField, context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionChip(
|
||||
SortField sortField, SortField? currentlySelectedOrder, BuildContext context) {
|
||||
String text;
|
||||
switch (sortField) {
|
||||
case SortField.archiveSerialNumber:
|
||||
text = S.of(context).documentArchiveSerialNumberPropertyShortLabel;
|
||||
break;
|
||||
case SortField.correspondentName:
|
||||
text = S.of(context).documentCorrespondentPropertyLabel;
|
||||
break;
|
||||
case SortField.title:
|
||||
text = S.of(context).documentTitlePropertyLabel;
|
||||
break;
|
||||
case SortField.documentType:
|
||||
text = S.of(context).documentDocumentTypePropertyLabel;
|
||||
break;
|
||||
case SortField.created:
|
||||
text = S.of(context).documentCreatedPropertyLabel;
|
||||
break;
|
||||
case SortField.added:
|
||||
text = S.of(context).documentAddedPropertyLabel;
|
||||
break;
|
||||
case SortField.modified:
|
||||
text = S.of(context).documentModifiedPropertyLabel;
|
||||
break;
|
||||
}
|
||||
|
||||
final docBloc = BlocProvider.of<DocumentsCubit>(context);
|
||||
return ActionChip(
|
||||
label: Text(text),
|
||||
avatar: currentlySelectedOrder == sortField
|
||||
? const Icon(
|
||||
Icons.done,
|
||||
color: Colors.green,
|
||||
)
|
||||
: null,
|
||||
onPressed: () =>
|
||||
docBloc.updateFilter(filter: docBloc.state.filter.copyWith(sortField: sortField)),
|
||||
);
|
||||
}
|
||||
|
||||
void _onApplyFilter() {
|
||||
setState(() => _isQueryLoading = true);
|
||||
_formKey.currentState?.save();
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final v = _formKey.currentState!.value;
|
||||
final docCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
DocumentFilter newFilter = docCubit.state.filter.copyWith(
|
||||
createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
|
||||
createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
|
||||
correspondent: v[fkCorrespondent] as CorrespondentQuery?,
|
||||
documentType: v[fkDocumentType] as DocumentTypeQuery?,
|
||||
storagePath: v[fkStoragePath] as StoragePathQuery?,
|
||||
tags: v[DocumentModel.tagsKey] as TagsQuery?,
|
||||
page: 1,
|
||||
queryText: v[fkQuery] as String?,
|
||||
addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end,
|
||||
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
|
||||
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
|
||||
);
|
||||
BlocProvider.of<DocumentsCubit>(context).updateFilter(filter: newFilter).then((value) {
|
||||
BlocProvider.of<SavedViewCubit>(context).resetSelection();
|
||||
FocusScope.of(context).unfocus();
|
||||
widget.panelController.close();
|
||||
setState(() => _isQueryLoading = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _patchFromFilter(DocumentFilter f) {
|
||||
_formKey.currentState?.patchValue({
|
||||
fkCorrespondent: f.correspondent,
|
||||
fkDocumentType: f.documentType,
|
||||
fkQuery: f.queryText,
|
||||
fkStoragePath: f.storagePath,
|
||||
DocumentModel.tagsKey: f.tags,
|
||||
DocumentModel.titleKey: f.queryText,
|
||||
QueryTypeFormField.fkQueryType: f.queryType,
|
||||
fkCreatedAt: _dateTimeRangeOfNullable(
|
||||
f.createdDateAfter,
|
||||
f.createdDateBefore,
|
||||
),
|
||||
fkAddedAt: _dateTimeRangeOfNullable(
|
||||
f.addedDateAfter,
|
||||
f.addedDateBefore,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/query_type.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class QueryTypeFormField extends StatelessWidget {
|
||||
static const fkQueryType = 'queryType';
|
||||
final QueryType? initialValue;
|
||||
final void Function(QueryType)? afterSelected;
|
||||
const QueryTypeFormField({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
this.afterSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<QueryType>(
|
||||
builder: (field) => PopupMenuButton<QueryType>(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text(S.of(context).documentsFilterPageQueryOptionsTitleAndContentLabel),
|
||||
),
|
||||
value: QueryType.titleAndContent,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text(S.of(context).documentsFilterPageQueryOptionsTitleLabel),
|
||||
),
|
||||
value: QueryType.title,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text(S.of(context).documentsFilterPageQueryOptionsExtendedLabel),
|
||||
),
|
||||
value: QueryType.extended,
|
||||
),
|
||||
//TODO: Add support for ASN queries
|
||||
],
|
||||
onSelected: (selection) {
|
||||
field.didChange(selection);
|
||||
afterSelected?.call(selection);
|
||||
},
|
||||
child: const Icon(Icons.more_vert),
|
||||
),
|
||||
initialValue: initialValue,
|
||||
name: QueryTypeFormField.fkQueryType,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/saved_view.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
class AddSavedViewPage extends StatefulWidget {
|
||||
final DocumentFilter currentFilter;
|
||||
const AddSavedViewPage({super.key, required this.currentFilter});
|
||||
|
||||
@override
|
||||
State<AddSavedViewPage> createState() => _AddSavedViewPageState();
|
||||
}
|
||||
|
||||
class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
||||
static const fkName = 'name';
|
||||
static const fkShowOnDashboard = 'show_on_dashboard';
|
||||
static const fkShowInSidebar = 'show_in_sidebar';
|
||||
|
||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).savedViewCreateNewLabel),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Tooltip(
|
||||
child: const Icon(Icons.info_outline),
|
||||
message: S.of(context).savedViewCreateTooltipText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _onCreate(context),
|
||||
label: Text(S.of(context).genericActionCreateLabel),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: fkName,
|
||||
validator: FormBuilderValidators.required(), //TODO: INTL
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context).savedViewNameLabel),
|
||||
),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: fkShowOnDashboard,
|
||||
initialValue: false,
|
||||
title: Text(S.of(context).savedViewShowOnDashboardLabel),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: fkShowInSidebar,
|
||||
initialValue: false,
|
||||
title: Text(S.of(context).savedViewShowInSidebarLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCreate(BuildContext context) {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
SavedView.fromDocumentFilter(
|
||||
widget.currentFilter,
|
||||
name: _formKey.currentState?.value[fkName] as String,
|
||||
showOnDashboard: _formKey.currentState?.value[fkShowOnDashboard] as bool,
|
||||
showInSidebar: _formKey.currentState?.value[fkShowInSidebar] as bool,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class BulkDeleteConfirmationDialog extends StatelessWidget {
|
||||
static const _bulletPoint = "\u2022";
|
||||
final DocumentsState state;
|
||||
const BulkDeleteConfirmationDialog({Key? key, required this.state})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(state.selection.isNotEmpty);
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).documentsPageSelectionBulkDeleteDialogTitle),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
//TODO: use plurals, didn't use because of crash... investigate later.
|
||||
state.selection.length == 1
|
||||
? S
|
||||
.of(context)
|
||||
.documentsPageSelectionBulkDeleteDialogWarningTextOne
|
||||
: S
|
||||
.of(context)
|
||||
.documentsPageSelectionBulkDeleteDialogWarningTextMany,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: state.selection.map(_buildBulletPoint).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(S.of(context).genericActionCancelLabel),
|
||||
),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(S.of(context).genericActionDeleteLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBulletPoint(DocumentModel doc) {
|
||||
return Text(
|
||||
"\t$_bulletPoint ${doc.title}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/saved_view.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
|
||||
class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
||||
const ConfirmDeleteSavedViewDialog({
|
||||
Key? key,
|
||||
required this.view,
|
||||
}) : super(key: key);
|
||||
|
||||
final SavedView view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
"Delete view " + view.name + "?",
|
||||
softWrap: true,
|
||||
),
|
||||
content: Text("Do you really want to delete this view?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(S.of(context).genericActionCancelLabel),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
S.of(context).genericActionDeleteLabel,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/selection/saved_view_selection_widget.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
|
||||
class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
|
||||
final List<Widget> actions;
|
||||
|
||||
const DocumentsPageAppBar({
|
||||
super.key,
|
||||
this.actions = const [],
|
||||
});
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
@override
|
||||
State<DocumentsPageAppBar> createState() => _DocumentsPageAppBarState();
|
||||
}
|
||||
|
||||
class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
|
||||
static const _flexibleAreaHeight = kToolbarHeight + 48.0;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, documentsState) {
|
||||
if (documentsState.selection.isNotEmpty) {
|
||||
return SliverAppBar(
|
||||
snap: true,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
expandedHeight: kToolbarHeight,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => BlocProvider.of<DocumentsCubit>(context).resetSelection(),
|
||||
),
|
||||
title:
|
||||
Text('${documentsState.selection.length} ${S.of(context).documentsSelectedText}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(context, documentsState),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SliverAppBar(
|
||||
expandedHeight: kToolbarHeight + _flexibleAreaHeight,
|
||||
pinned: true,
|
||||
flexibleSpace: const FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: SavedViewSelectionWidget(height: _flexibleAreaHeight),
|
||||
),
|
||||
),
|
||||
title: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return Text(
|
||||
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})',
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: widget.actions,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => BulkDeleteConfirmationDialog(state: documentsState),
|
||||
);
|
||||
if (shouldDelete ?? false) {
|
||||
BlocProvider.of<DocumentsCubit>(context)
|
||||
.bulkRemoveDocuments(documentsState.selection)
|
||||
.then((_) => showSnackBar(context, S.of(context).documentsPageBulkDeleteSuccessfulText))
|
||||
.onError<ErrorMessage>(
|
||||
(error, _) => showSnackBar(context, translateError(context, error.code)));
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDocumentCount(int count) {
|
||||
return count > 99 ? "99+" : count.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/saved_view.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/selection/add_saved_view_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
|
||||
class SavedViewSelectionWidget extends StatelessWidget {
|
||||
const SavedViewSelectionWidget({
|
||||
Key? key,
|
||||
required this.height,
|
||||
}) : super(key: key);
|
||||
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
if (state.value.isEmpty) {
|
||||
return Text(S.of(context).savedViewsEmptyStateText);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 48.0,
|
||||
child: ListView.separated(
|
||||
itemCount: state.value.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final view = state.value.values.elementAt(index);
|
||||
return GestureDetector(
|
||||
onLongPress: () => _onDelete(context, view),
|
||||
child: FilterChip(
|
||||
label: Text(state.value.values.toList()[index].name),
|
||||
selected: view.id == state.selectedSavedViewId,
|
||||
onSelected: (isSelected) => _onSelected(isSelected, context, view),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).savedViewsLabel,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _onCreatePressed(context),
|
||||
label: Text(S.of(context).savedViewCreateNewLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onCreatePressed(BuildContext context) async {
|
||||
final newView = await Navigator.of(context).push<SavedView?>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddSavedViewPage(currentFilter: getIt<DocumentsCubit>().state.filter),
|
||||
),
|
||||
);
|
||||
if (newView != null) {
|
||||
try {
|
||||
BlocProvider.of<SavedViewCubit>(context).add(newView);
|
||||
} on ErrorMessage catch (error) {
|
||||
showError(context, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelected(bool isSelected, BuildContext context, SavedView view) {
|
||||
if (isSelected) {
|
||||
BlocProvider.of<DocumentsCubit>(context).updateFilter(filter: view.toDocumentFilter());
|
||||
BlocProvider.of<SavedViewCubit>(context).selectView(view);
|
||||
} else {
|
||||
BlocProvider.of<DocumentsCubit>(context).updateFilter();
|
||||
BlocProvider.of<SavedViewCubit>(context).selectView(null);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDelete(BuildContext context, SavedView view) async {
|
||||
{
|
||||
final delete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ConfirmDeleteSavedViewDialog(view: view),
|
||||
) ??
|
||||
false;
|
||||
if (delete) {
|
||||
try {
|
||||
BlocProvider.of<SavedViewCubit>(context).remove(view);
|
||||
} on ErrorMessage catch (error) {
|
||||
showError(context, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/sort_order.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class SortDocumentsButton extends StatefulWidget {
|
||||
const SortDocumentsButton({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SortDocumentsButton> createState() => _SortDocumentsButtonState();
|
||||
}
|
||||
|
||||
class _SortDocumentsButtonState extends State<SortDocumentsButton> {
|
||||
bool _isLoading = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
Widget child;
|
||||
if (_isLoading) {
|
||||
child = const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: RefreshProgressIndicator(
|
||||
strokeWidth: 4.0,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final bool isAscending = state.filter.sortOrder == SortOrder.ascending;
|
||||
child = IconButton(
|
||||
icon: FaIcon(
|
||||
isAscending ? FontAwesomeIcons.arrowDownAZ : FontAwesomeIcons.arrowUpZA,
|
||||
),
|
||||
onPressed: () async {
|
||||
setState(() => _isLoading = true);
|
||||
BlocProvider.of<DocumentsCubit>(context)
|
||||
.updateFilter(
|
||||
filter: state.filter.copyWith(sortOrder: state.filter.sortOrder.toggle()))
|
||||
.whenComplete(() => setState(() => _isLoading = false));
|
||||
},
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: Theme.of(context).iconTheme.size,
|
||||
width: Theme.of(context).iconTheme.size,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user