import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/accessibility/accessibility_utils.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.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/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/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/connectivity_aware_action_wrapper.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/routing/routes/documents_route.dart'; import 'package:paperless_mobile/routing/routes/shells/authenticated_route.dart'; import 'package:paperless_mobile/theme.dart'; class DocumentDetailsPage extends StatefulWidget { final int id; final String? title; final bool isLabelClickable; final String? titleAndContentQueryString; final String? thumbnailUrl; final String? heroTag; const DocumentDetailsPage({ Key? key, this.isLabelClickable = true, this.titleAndContentQueryString, this.thumbnailUrl, required this.id, this.heroTag, this.title, }) : super(key: key); @override State createState() => _DocumentDetailsPageState(); } class _DocumentDetailsPageState extends State { static const double _itemSpacing = 24; final _pagingScrollController = ScrollController(); @override void didChangeDependencies() { super.didChangeDependencies(); initializeDateFormatting(Localizations.localeOf(context).toString()); } @override Widget build(BuildContext context) { final disableAnimations = MediaQuery.disableAnimationsOf(context); debugPrint(disableAnimations.toString()); final hasMultiUserSupport = context.watch().hasMultiUserSupport; final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0); return AnnotatedRegion( value: buildOverlayStyle( Theme.of(context), systemNavigationBarColor: Theme.of(context).bottomAppBarTheme.color, ), child: BlocBuilder( builder: (context, state) { return DefaultTabController( length: tabLength, child: Scaffold( extendBodyBehindAppBar: false, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButton: switch (state) { DocumentDetailsLoaded(document: var document) => _buildEditButton(document), _ => null }, bottomNavigationBar: _buildBottomAppBar(), body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context), sliver: BlocBuilder( builder: (context, state) { final title = switch (state) { DocumentDetailsLoaded(document: var document) => document.title, _ => widget.title ?? '', }; return SliverAppBar( title: Text(title), leading: const BackButton(), pinned: true, forceElevated: innerBoxIsScrolled, collapsedHeight: kToolbarHeight, expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( background: Builder( builder: (context) { return Hero( tag: widget.heroTag ?? "thumb_${widget.id}", child: GestureDetector( onTap: () { DocumentPreviewRoute( id: widget.id, title: title, ).push(context); }, child: Stack( alignment: Alignment.topCenter, children: [ Positioned.fill( child: DocumentPreview( documentId: widget.id, title: title, enableHero: false, fit: BoxFit.cover, alignment: Alignment.topCenter, ), ), Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( stops: [0.2, 0.4], colors: [ Theme.of(context) .colorScheme .background .withOpacity(0.6), Theme.of(context) .colorScheme .background .withOpacity(0.3), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ), ], ), ), ).accessible(); }, ), ), 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, ), ), ), // if (hasMultiUserSupport && false) // Tab( // child: Text( // "Permissions", // style: TextStyle( // color: Theme.of(context) // .colorScheme // .onPrimaryContainer, // ), // ), // ), ], ), ), ); }, ), ), ], body: BlocBuilder( builder: (context, state) { return BlocProvider( create: (context) => SimilarDocumentsCubit( context.read(), context.read(), context.read(), context.read(), documentId: widget.id, ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 16, ), child: TabBarView( children: [ CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), switch (state) { DocumentDetailsLoaded( document: var document ) => DocumentOverviewWidget( document: document, itemSpacing: _itemSpacing, queryString: widget.titleAndContentQueryString, ), DocumentDetailsError() => _buildErrorState(), _ => _buildLoadingState(), }, ], ), CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), switch (state) { DocumentDetailsLoaded( document: var document ) => DocumentContentWidget( document: document, queryString: widget.titleAndContentQueryString, ), DocumentDetailsError() => _buildErrorState(), _ => _buildLoadingState(), } ], ), CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), switch (state) { DocumentDetailsLoaded( document: var document, metaData: var metaData, ) => DocumentMetaDataWidget( document: document, itemSpacing: _itemSpacing, metaData: metaData, ), DocumentDetailsError() => _buildErrorState(), _ => _buildLoadingState(), }, ], ), CustomScrollView( controller: _pagingScrollController, slivers: [ SliverOverlapInjector( handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), SimilarDocumentsView( pagingScrollController: _pagingScrollController, ), ], ), // if (hasMultiUserSupport && false) // CustomScrollView( // controller: _pagingScrollController, // slivers: [ // SliverOverlapInjector( // handle: NestedScrollView // .sliverOverlapAbsorberHandleFor( // context), // ), // DocumentPermissionsWidget( // document: state.document, // ), // ], // ), ], ), ), ); }, ), ), ), ); }, ), ); } Widget _buildEditButton(DocumentModel document) { final currentUser = context.watch(); bool canEdit = context.watchInternetConnection && currentUser.paperlessUser.canEditDocuments; if (!canEdit) { return const SizedBox.shrink(); } return Tooltip( message: S.of(context)!.editDocumentTooltip, preferBelow: false, verticalOffset: 40, child: FloatingActionButton( heroTag: "fab_document_details", child: const Icon(Icons.edit), onPressed: () => EditDocumentRoute(document).push(context), ), ); } Widget _buildErrorState() { return SliverToBoxAdapter( child: Center( child: Text("Could not load document."), ), ); } Widget _buildLoadingState() { return SliverFillRemaining( child: Center( child: CircularProgressIndicator(), ), ); } BlocBuilder _buildBottomAppBar() { return BlocBuilder( builder: (context, state) { final currentUser = context.watch(); return BottomAppBar( child: Builder( builder: (context) { return switch (state) { DocumentDetailsLoaded(document: var document) => Row( mainAxisAlignment: MainAxisAlignment.start, children: [ ConnectivityAwareActionWrapper( disabled: !currentUser.paperlessUser.canDeleteDocuments, offlineBuilder: (context, child) { return const IconButton( icon: Icon(Icons.delete), onPressed: null, ).paddedSymmetrically(horizontal: 4); }, child: IconButton( tooltip: S.of(context)!.deleteDocumentTooltip, icon: const Icon(Icons.delete), onPressed: () => _onDelete(document), ).paddedSymmetrically(horizontal: 4), ), ConnectivityAwareActionWrapper( offlineBuilder: (context, child) => const DocumentDownloadButton( document: null, enabled: false, ), child: DocumentDownloadButton( document: document, ), ), ConnectivityAwareActionWrapper( offlineBuilder: (context, child) => const IconButton( icon: Icon(Icons.open_in_new), onPressed: null, ), child: IconButton( tooltip: S.of(context)!.openInSystemViewer, icon: const Icon(Icons.open_in_new), onPressed: _onOpenFileInSystemViewer, ).paddedOnly(right: 4.0), ), DocumentShareButton(document: document), IconButton( tooltip: S.of(context)!.print, onPressed: () => context .read() .printDocument(), icon: const Icon(Icons.print), ), ], ), _ => SizedBox.shrink(), }; }, ), ); }, ); } 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 PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { do { context.pop(); } while (context.canPop()); } } } }