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/database/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/features/logging/data/logger.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/helpers/connectivity_aware_action_wrapper.dart'; import 'package:paperless_mobile/routing/routes/labels_route.dart'; import 'package:paperless_mobile/routing/routes/shells/authenticated_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 void dispose() { _tabController.dispose(); super.dispose(); } @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; final fabLabel = [ S.of(context)!.addCorrespondent, S.of(context)!.addDocumentType, S.of(context)!.addTag, S.of(context)!.addStoragePath, ][_currentIndex]; return BlocBuilder( builder: (context, connectedState) { return SafeArea( child: Scaffold( drawer: const AppDrawer(), floatingActionButton: ConnectivityAwareActionWrapper( offlineBuilder: (context, child) => const SizedBox.shrink(), child: FloatingActionButton.extended( heroTag: "inbox_page_fab", label: Text(fabLabel), icon: Icon(Icons.add), 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], ), ), 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) { logger.fe( "An error ocurred while reloading " "${[ "correspondents", "document types", "tags", "storage paths" ][_currentIndex]}.", error: error, stackTrace: stackTrace, className: runtimeType.toString(), methodName: 'onRefresh'); } }, 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: SetIdQueryParameter(id: 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: SetIdQueryParameter(id: 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: IdsTagsQuery(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: SetIdQueryParameter(id: 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), ), ], ); }, ); } }