import 'package:badges/badges.dart' as b; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart'; import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; class DocumentDetailsPage extends StatefulWidget { final bool allowEdit; final bool isLabelClickable; final String? titleAndContentQueryString; const DocumentDetailsPage({ Key? key, this.isLabelClickable = true, this.titleAndContentQueryString, this.allowEdit = true, }) : super(key: key); @override State createState() => _DocumentDetailsPageState(); } class _DocumentDetailsPageState extends State { late Future _metaData; static const double _itemSpacing = 24; final _pagingScrollController = ScrollController(); @override void initState() { super.initState(); _loadMetaData(); } void _loadMetaData() { _metaData = context .read() .getMetaData(context.read().state.document); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { Navigator.of(context).pop(context.read().state.document); return false; }, child: DefaultTabController( length: 4, child: BlocListener( listenWhen: (previous, current) => !previous.isConnected && current.isConnected, listener: (context, state) { _loadMetaData(); setState(() {}); }, child: Scaffold( extendBodyBehindAppBar: false, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButton: widget.allowEdit ? _buildEditButton() : null, bottomNavigationBar: _buildBottomAppBar(), body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: Text(context.watch().state.document.title), leading: const BackButton(), pinned: true, forceElevated: innerBoxIsScrolled, collapsedHeight: kToolbarHeight, expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( background: Stack( alignment: Alignment.topCenter, children: [ BlocBuilder( builder: (context, state) => Positioned.fill( child: DocumentPreview( document: state.document, fit: BoxFit.cover, ), ), ), Positioned.fill( top: 0, child: Container( height: 100, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.black.withOpacity(0.7), Colors.black.withOpacity(0.2), Colors.transparent, Colors.transparent, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ), ], ), ), bottom: ColoredTabBar( tabBar: TabBar( isScrollable: true, tabs: [ Tab( child: Text( S.of(context)!.overview, style: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), Tab( child: Text( S.of(context)!.content, style: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), Tab( child: Text( S.of(context)!.metaData, style: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), Tab( child: Text( S.of(context)!.similarDocuments, style: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), ], ), ), ), ), ], body: BlocBuilder( builder: (context, state) { return BlocProvider( create: (context) => SimilarDocumentsCubit( context.read(), context.read(), context.read(), documentId: state.document.id, ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 16, ), child: TabBarView( children: [ CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), DocumentOverviewWidget( document: state.document, itemSpacing: _itemSpacing, queryString: widget.titleAndContentQueryString, availableCorrespondents: state.correspondents, availableDocumentTypes: state.documentTypes, availableTags: state.tags, availableStoragePaths: state.storagePaths, ), ], ), CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), DocumentContentWidget( isFullContentLoaded: state.isFullContentLoaded, document: state.document, fullContent: state.fullContent, queryString: widget.titleAndContentQueryString, ), ], ), CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), DocumentMetaDataWidget( document: state.document, itemSpacing: _itemSpacing, ), ], ), CustomScrollView( controller: _pagingScrollController, slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SimilarDocumentsView( pagingScrollController: _pagingScrollController, ), ], ), ], ), ), ); }, ), ), ), ), ), ); } Widget _buildEditButton() { return BlocBuilder( builder: (context, state) { // final _filteredSuggestions = // state.suggestions?.documentDifference(state.document); return BlocBuilder( builder: (context, connectivityState) { if (!connectivityState.isConnected) { return const SizedBox.shrink(); } return Tooltip( message: S.of(context)!.editDocumentTooltip, preferBelow: false, verticalOffset: 40, child: FloatingActionButton( child: const Icon(Icons.edit), onPressed: () => _onEdit(state.document), ), ); }, ); }, ); } BlocBuilder _buildBottomAppBar() { return BlocBuilder( builder: (context, state) { return BottomAppBar( child: BlocBuilder( builder: (context, connectivityState) { final isConnected = connectivityState.isConnected; return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ IconButton( tooltip: S.of(context)!.deleteDocumentTooltip, icon: const Icon(Icons.delete), onPressed: widget.allowEdit && isConnected ? () => _onDelete(state.document) : null, ).paddedSymmetrically(horizontal: 4), DocumentDownloadButton( document: state.document, enabled: isConnected, metaData: _metaData, ), IconButton( tooltip: S.of(context)!.previewTooltip, icon: const Icon(Icons.visibility), onPressed: isConnected ? () => _onOpen(state.document) : null, ).paddedOnly(right: 4.0), IconButton( tooltip: S.of(context)!.openInSystemViewer, icon: const Icon(Icons.open_in_new), onPressed: isConnected ? _onOpenFileInSystemViewer : null, ).paddedOnly(right: 4.0), DocumentShareButton(document: state.document), ], ); }, ), ); }, ); } Future _onEdit(DocumentModel document) async { { final cubit = context.read(); Navigator.push( context, MaterialPageRoute( builder: (_) => MultiBlocProvider( providers: [ BlocProvider.value( value: DocumentEditCubit( context.read(), context.read(), context.read(), document: document, ), ), BlocProvider.value( value: cubit, ), ], child: BlocListener( listenWhen: (previous, current) => previous.document != current.document, listener: (context, state) { cubit.replace(state.document); }, child: BlocBuilder( builder: (context, state) { return DocumentEditPage( suggestions: state.suggestions, ); }, ), ), ), maintainState: true, ), ); } } void _onOpenFileInSystemViewer() async { final status = await context.read().openDocumentInSystemViewer(); if (status == ResultType.done) return; if (status == ResultType.noAppToOpen) { showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound); } if (status == ResultType.fileNotFound) { showGenericError(context, translateError(context, ErrorCode.unknown)); } if (status == ResultType.permissionDenied) { showGenericError(context, S.of(context)!.couldNotOpenFilePermissionDenied); } } void _onDelete(DocumentModel document) async { final delete = await showDialog( context: context, builder: (context) => DeleteDocumentConfirmationDialog(document: document), ) ?? false; if (delete) { try { await context.read().delete(document); showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { // Document deleted => go back to primary route Navigator.popUntil(context, (route) => route.isFirst); } } } Future _onOpen(DocumentModel document) async { Navigator.of(context).push( MaterialPageRoute( builder: (context) => DocumentView( documentBytes: context.read().getPreview(document.id), ), ), ); } }