mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 10:08:02 -06:00
feat: Rework error handling, upgrade dio, fixed bugs
- Fix grey screen bug when adding labels from documnet upload - Add more permission checks to conditionally show widgets
This commit is contained in:
@@ -73,10 +73,7 @@ class TagsFormField extends StatelessWidget {
|
||||
initialValue: field.value,
|
||||
allowOnlySelection: allowOnlySelection,
|
||||
allowCreation: allowCreation &&
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.tag,
|
||||
),
|
||||
LocalUserAccount.current.paperlessUser.canCreateTags,
|
||||
allowExclude: allowExclude,
|
||||
),
|
||||
onClosed: (data) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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/repository/label_repository.dart';
|
||||
@@ -39,291 +42,327 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
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();
|
||||
|
||||
_tabController = TabController(length: 4, vsync: this)
|
||||
final user = LocalUserAccount.current.paperlessUser;
|
||||
_tabController = TabController(
|
||||
length: _calculateTabCount(user), vsync: this)
|
||||
..addListener(() => setState(() => _currentIndex = _tabController.index));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: [
|
||||
_openAddCorrespondentPage,
|
||||
_openAddDocumentTypePage,
|
||||
_openAddTagPage,
|
||||
_openAddStoragePathPage,
|
||||
][_currentIndex],
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: const SliverSearchBar(),
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
builder: (context, box, child) {
|
||||
final currentUserId =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser;
|
||||
final user = box.get(currentUserId)!.paperlessUser;
|
||||
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: [
|
||||
if (user.canViewCorrespondents) _openAddCorrespondentPage,
|
||||
if (user.canViewDocumentTypes) _openAddDocumentTypePage,
|
||||
if (user.canViewTags) _openAddTagPage,
|
||||
if (user.canViewStoragePaths) _openAddStoragePathPage,
|
||||
][_currentIndex],
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
SliverOverlapAbsorber(
|
||||
handle: tabBarHandle,
|
||||
sliver: SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||
child: ColoredTabBar(
|
||||
tabBar: TabBar(
|
||||
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<LabelCubit, LabelState>(
|
||||
builder: (context, state) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
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<LabelCubit>()
|
||||
.reloadCorrespondents,
|
||||
context
|
||||
.read<LabelCubit>()
|
||||
.reloadDocumentTypes,
|
||||
context.read<LabelCubit>().reloadTags,
|
||||
context.read<LabelCubit>().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,
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
children: [
|
||||
if (user.canViewCorrespondents)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<Correspondent>(
|
||||
labels: state.correspondents,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
correspondent:
|
||||
IdQueryParameter.fromId(
|
||||
label.id!),
|
||||
),
|
||||
canEdit: user.canEditCorrespondents,
|
||||
canAddNew:
|
||||
user.canCreateCorrespondents,
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)!
|
||||
.addNewCorrespondent,
|
||||
emptyStateDescription: S
|
||||
.of(context)!
|
||||
.noCorrespondentsSetUp,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.description_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
if (user.canViewDocumentTypes)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<DocumentType>(
|
||||
labels: state.documentTypes,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
documentType:
|
||||
IdQueryParameter.fromId(
|
||||
label.id!),
|
||||
),
|
||||
canEdit: user.canEditDocumentTypes,
|
||||
canAddNew:
|
||||
user.canCreateDocumentTypes,
|
||||
onEdit: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)!
|
||||
.addNewDocumentType,
|
||||
emptyStateDescription: S
|
||||
.of(context)!
|
||||
.noDocumentTypesSetUp,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.label_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
if (user.canViewTags)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<Tag>(
|
||||
labels: state.tags,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
tags: TagsQuery.ids(
|
||||
include: [label.id!]),
|
||||
),
|
||||
canEdit: user.canEditTags,
|
||||
canAddNew: user.canCreateTags,
|
||||
onEdit: _openEditTagPage,
|
||||
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: _openAddTagPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.folder_open,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
if (user.canViewStoragePaths)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<StoragePath>(
|
||||
labels: state.storagePaths,
|
||||
onEdit: _openEditStoragePathPage,
|
||||
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: _openAddStoragePathPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
minExtent: kTextTabBarHeight,
|
||||
maxExtent: kTextTabBarHeight),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
body: BlocBuilder<LabelCubit, LabelState>(
|
||||
builder: (context, state) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
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<LabelCubit>().reloadCorrespondents,
|
||||
context.read<LabelCubit>().reloadDocumentTypes,
|
||||
context.read<LabelCubit>().reloadTags,
|
||||
context.read<LabelCubit>().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: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<Correspondent>(
|
||||
labels: state.correspondents,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
correspondent:
|
||||
IdQueryParameter.fromId(label.id!),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.correspondent),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.correspondent),
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewCorrespondent,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noCorrespondentsSetUp,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<DocumentType>(
|
||||
labels: state.documentTypes,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
documentType:
|
||||
IdQueryParameter.fromId(label.id!),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.documentType),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.documentType),
|
||||
onEdit: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewDocumentType,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noDocumentTypesSetUp,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<Tag>(
|
||||
labels: state.tags,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
tags:
|
||||
TagsQuery.ids(include: [label.id!]),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.tag),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.tag),
|
||||
onEdit: _openEditTagPage,
|
||||
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: _openAddTagPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<StoragePath>(
|
||||
labels: state.storagePaths,
|
||||
onEdit: _openEditStoragePathPage,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
storagePath:
|
||||
IdQueryParameter.fromId(label.id!),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.storagePath),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.storagePath),
|
||||
contentBuilder: (path) => Text(path.path),
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewStoragePath,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noStoragePathsSetUp,
|
||||
onAddNew: _openAddStoragePathPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _openEditCorrespondentPage(Correspondent correspondent) {
|
||||
|
||||
@@ -36,8 +36,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
||||
|
||||
Widget _buildReferencedDocumentsWidget(BuildContext context) {
|
||||
final canOpen = (label.documentCount ?? 0) > 0 &&
|
||||
LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.view, PermissionTarget.document);
|
||||
LocalUserAccount.current.paperlessUser.canViewDocuments;
|
||||
return TextButton.icon(
|
||||
label: const Icon(Icons.link),
|
||||
icon: Text(formatMaxCount(label.documentCount)),
|
||||
|
||||
Reference in New Issue
Block a user