import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/routes/typed/branches/labels_route.dart'; class LabelsPage extends StatefulWidget { const LabelsPage({Key? key}) : super(key: key); @override State createState() => _LabelsPageState(); } class _LabelsPageState extends State with SingleTickerProviderStateMixin { final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle(); late final TabController _tabController; int _currentIndex = 0; int _calculateTabCount(UserModel user) => [ user.canViewCorrespondents, user.canViewDocumentTypes, user.canViewTags, user.canViewStoragePaths, ].fold(0, (value, element) => value + (element ? 1 : 0)); @override void initState() { super.initState(); final user = context.read().paperlessUser; _tabController = TabController( length: _calculateTabCount(user), vsync: this) ..addListener(() => setState(() => _currentIndex = _tabController.index)); } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: Hive.box(HiveBoxes.localUserAccount).listenable(), builder: (context, box, child) { final currentUserId = Hive.box(HiveBoxes.globalSettings) .getValue()! .loggedInUserId; final user = box.get(currentUserId)!.paperlessUser; return BlocBuilder( builder: (context, connectedState) { return SafeArea( child: Scaffold( drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( heroTag: "fab_labels_page", onPressed: [ if (user.canViewCorrespondents) () => CreateLabelRoute(LabelType.correspondent) .push(context), if (user.canViewDocumentTypes) () => CreateLabelRoute(LabelType.documentType) .push(context), if (user.canViewTags) () => CreateLabelRoute(LabelType.tag).push(context), if (user.canViewStoragePaths) () => CreateLabelRoute(LabelType.storagePath) .push(context), ][_currentIndex], child: const Icon(Icons.add), ), body: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: searchBarHandle, sliver: SliverSearchBar( titleText: S.of(context)!.labels, ), ), SliverOverlapAbsorber( handle: tabBarHandle, sliver: SliverPersistentHeader( pinned: true, delegate: CustomizableSliverPersistentHeaderDelegate( child: ColoredTabBar( tabBar: TabBar( controller: _tabController, tabs: [ if (user.canViewCorrespondents) Tab( icon: Tooltip( message: S.of(context)!.correspondents, child: Icon( Icons.person_outline, color: Theme.of(context) .colorScheme .onPrimaryContainer, ), ), ), if (user.canViewDocumentTypes) Tab( icon: Tooltip( message: S.of(context)!.documentTypes, child: Icon( Icons.description_outlined, color: Theme.of(context) .colorScheme .onPrimaryContainer, ), ), ), if (user.canViewTags) Tab( icon: Tooltip( message: S.of(context)!.tags, child: Icon( Icons.label_outline, color: Theme.of(context) .colorScheme .onPrimaryContainer, ), ), ), if (user.canViewStoragePaths) Tab( icon: Tooltip( message: S.of(context)!.storagePaths, child: Icon( Icons.folder_open, color: Theme.of(context) .colorScheme .onPrimaryContainer, ), ), ), ], ), ), minExtent: kTextTabBarHeight, maxExtent: kTextTabBarHeight, ), ), ), ], body: BlocBuilder( builder: (context, state) { return NotificationListener( onNotification: (notification) { final metrics = notification.metrics; if (metrics.maxScrollExtent == 0) { return true; } final desiredTab = ((metrics.pixels / metrics.maxScrollExtent) * (_tabController.length - 1)) .round(); if (metrics.axis == Axis.horizontal && _currentIndex != desiredTab) { setState(() => _currentIndex = desiredTab); } return true; }, child: RefreshIndicator( edgeOffset: kTextTabBarHeight, notificationPredicate: (notification) => connectedState.isConnected, onRefresh: () async { try { await [ context .read() .reloadCorrespondents, context .read() .reloadDocumentTypes, context.read().reloadTags, context.read().reloadStoragePaths, ][_currentIndex] .call(); } catch (error, stackTrace) { debugPrint( "[LabelsPage] RefreshIndicator.onRefresh " "${[ "correspondents", "document types", "tags", "storage paths" ][_currentIndex]}: " "An error occurred (${error.toString()})", ); debugPrintStack(stackTrace: stackTrace); } }, child: TabBarView( controller: _tabController, children: [ if (user.canViewCorrespondents) _buildCorrespondentsView(state, user), if (user.canViewDocumentTypes) _buildDocumentTypesView(state, user), if (user.canViewTags) _buildTagsView(state, user), if (user.canViewStoragePaths) _buildStoragePathView(state, user), ], ), ), ); }, ), ), ), ); }, ); }); } Widget _buildCorrespondentsView(LabelState state, UserModel user) { return Builder( builder: (context) { return CustomScrollView( slivers: [ SliverOverlapInjector(handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.correspondents, filterBuilder: (label) => DocumentFilter( correspondent: IdQueryParameter.fromId(label.id!), ), canEdit: user.canEditCorrespondents, canAddNew: user.canCreateCorrespondents, onEdit: (correspondent) { EditLabelRoute(correspondent).push(context); }, emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent, emptyStateDescription: S.of(context)!.noCorrespondentsSetUp, onAddNew: () => CreateLabelRoute(LabelType.correspondent).push(context), ), ], ); }, ); } Widget _buildDocumentTypesView(LabelState state, UserModel user) { return Builder( builder: (context) { return CustomScrollView( slivers: [ SliverOverlapInjector(handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.documentTypes, filterBuilder: (label) => DocumentFilter( documentType: IdQueryParameter.fromId(label.id!), ), canEdit: user.canEditDocumentTypes, canAddNew: user.canCreateDocumentTypes, onEdit: (label) { EditLabelRoute(label).push(context); }, emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType, emptyStateDescription: S.of(context)!.noDocumentTypesSetUp, onAddNew: () => CreateLabelRoute(LabelType.documentType).push(context), ), ], ); }, ); } Widget _buildTagsView(LabelState state, UserModel user) { return Builder( builder: (context) { return CustomScrollView( slivers: [ SliverOverlapInjector(handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.tags, filterBuilder: (label) => DocumentFilter( tags: TagsQuery.ids(include: [label.id!]), ), canEdit: user.canEditTags, canAddNew: user.canCreateTags, onEdit: (label) { EditLabelRoute(label).push(context); }, leadingBuilder: (t) => CircleAvatar( backgroundColor: t.color, child: t.isInboxTag ? Icon( Icons.inbox, color: t.textColor, ) : null, ), emptyStateActionButtonLabel: S.of(context)!.addNewTag, emptyStateDescription: S.of(context)!.noTagsSetUp, onAddNew: () => CreateLabelRoute(LabelType.tag).push(context), ), ], ); }, ); } Widget _buildStoragePathView(LabelState state, UserModel user) { return Builder( builder: (context) { return CustomScrollView( slivers: [ SliverOverlapInjector(handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.storagePaths, onEdit: (label) { EditLabelRoute(label).push(context); }, filterBuilder: (label) => DocumentFilter( storagePath: IdQueryParameter.fromId(label.id!), ), canEdit: user.canEditStoragePaths, canAddNew: user.canCreateStoragePaths, contentBuilder: (path) => Text(path.path), emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath, emptyStateDescription: S.of(context)!.noStoragePathsSetUp, onAddNew: () => CreateLabelRoute(LabelType.storagePath).push(context), ), ], ); }, ); } }