diff --git a/lib/core/config/hive/hive_extensions.dart b/lib/core/config/hive/hive_extensions.dart new file mode 100644 index 0000000..09913ce --- /dev/null +++ b/lib/core/config/hive/hive_extensions.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive_flutter/adapters.dart'; + +/// +/// Opens an encrypted box, calls [callback] with the now opened box, awaits +/// [callback] to return and returns the calculated value. Closes the box after. +/// +Future withEncryptedBox(String name, FutureOr Function(Box box) callback) async { + final key = await _getEncryptedBoxKey(); + final box = await Hive.openBox( + name, + encryptionCipher: HiveAesCipher(key), + ); + final result = await callback(box); + await box.close(); + return result; +} + +Future _getEncryptedBoxKey() async { + const secureStorage = FlutterSecureStorage(); + if (!await secureStorage.containsKey(key: 'key')) { + final key = Hive.generateSecureKey(); + + await secureStorage.write( + key: 'key', + value: base64UrlEncode(key), + ); + } + final key = (await secureStorage.read(key: 'key'))!; + return base64Decode(key); +} diff --git a/lib/core/navigation/push_routes.dart b/lib/core/navigation/push_routes.dart index 02f296d..4d7cb05 100644 --- a/lib/core/navigation/push_routes.dart +++ b/lib/core/navigation/push_routes.dart @@ -15,7 +15,6 @@ import 'package:paperless_mobile/core/repository/user_repository.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; -import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; @@ -38,6 +37,7 @@ import 'package:provider/provider.dart'; Future pushDocumentSearchPage(BuildContext context) { final currentUser = Hive.box(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; + final userRepo = context.read(); return Navigator.of(context).push( MaterialPageRoute( builder: (_) => MultiProvider( @@ -46,6 +46,7 @@ Future pushDocumentSearchPage(BuildContext context) { Provider.value(value: context.read()), Provider.value(value: context.read()), Provider.value(value: context.read()), + Provider.value(value: userRepo), ], builder: (context, _) { return BlocProvider( diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart index 16d8f36..19778a7 100644 --- a/lib/features/app_drawer/view/app_drawer.dart +++ b/lib/features/app_drawer/view/app_drawer.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -92,8 +93,11 @@ class AppDrawer extends StatelessWidget { ), onTap: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => Provider.value( - value: context.read(), + builder: (_) => MultiProvider( + providers: [ + Provider.value(value: context.read()), + Provider.value(value: context.read()), + ], child: const SettingsPage(), ), ), diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 826a08e..0d7396c 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -240,7 +240,9 @@ class _DocumentDetailsPageState extends State { SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), - const DocumentPermissionsWidget(), + DocumentPermissionsWidget( + document: state.document, + ), ], ), ], diff --git a/lib/features/document_details/view/widgets/document_permissions_widget.dart b/lib/features/document_details/view/widgets/document_permissions_widget.dart index 655fbf6..c2621c0 100644 --- a/lib/features/document_details/view/widgets/document_permissions_widget.dart +++ b/lib/features/document_details/view/widgets/document_permissions_widget.dart @@ -1,8 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:paperless_api/paperless_api.dart'; -class DocumentPermissionsWidget extends StatelessWidget { - const DocumentPermissionsWidget({super.key}); +class DocumentPermissionsWidget extends StatefulWidget { + final DocumentModel document; + const DocumentPermissionsWidget({super.key, required this.document}); + @override + State createState() => _DocumentPermissionsWidgetState(); +} + +class _DocumentPermissionsWidgetState extends State { @override Widget build(BuildContext context) { return const SliverToBoxAdapter( diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index c4426a2..1f2b8c6 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -18,8 +18,6 @@ import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; -import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; -import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart index 1150d1e..95e8285 100644 --- a/lib/features/document_search/view/document_search_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -27,88 +27,89 @@ class DocumentSearchBar extends StatefulWidget { class _DocumentSearchBarState extends State { @override Widget build(BuildContext context) { - return OpenContainer( - transitionDuration: const Duration(milliseconds: 200), - transitionType: ContainerTransitionType.fadeThrough, - closedElevation: 1, - middleColor: Theme.of(context).colorScheme.surfaceVariant, - openColor: Theme.of(context).colorScheme.background, - closedColor: Theme.of(context).colorScheme.surfaceVariant, - closedShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(56), - ), - closedBuilder: (_, action) { - return InkWell( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 720, - minWidth: 360, - maxHeight: 56, - minHeight: 48, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - IconButton( - icon: Icon(Icons.menu), - onPressed: Scaffold.of(context).openDrawer, - ), - Expanded( - child: Hero( - tag: "search_hero_tag", - child: TextField( - enabled: false, - decoration: InputDecoration( - border: InputBorder.none, - hintText: S.of(context)!.searchDocuments, - hintStyle: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), + return Container( + margin: EdgeInsets.only(top: 8), + child: OpenContainer( + transitionDuration: const Duration(milliseconds: 200), + transitionType: ContainerTransitionType.fadeThrough, + closedElevation: 1, + middleColor: Theme.of(context).colorScheme.surfaceVariant, + openColor: Theme.of(context).colorScheme.background, + closedColor: Theme.of(context).colorScheme.surfaceVariant, + closedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(56), + ), + closedBuilder: (_, action) { + return InkWell( + onTap: action, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 720, + minWidth: 360, + maxHeight: 56, + minHeight: 48, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + Flexible( + child: Text( + S.of(context)!.searchDocuments, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).hintColor, + ), ), ), - ), - ], + ], + ), ), ), - ), - _buildUserAvatar(context), - ], + _buildUserAvatar(context), + ], + ), ), - ), - ); - }, - openBuilder: (_, action) { - return MultiProvider( - providers: [ - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - ], - child: Provider( - create: (_) => DocumentSearchCubit( - context.read(), - context.read(), - context.read(), - Hive.box(HiveBoxes.localUserAppState) - .get(LocalUserAccount.current.id)!, + ); + }, + openBuilder: (_, action) { + return MultiProvider( + providers: [ + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + if (context.read().hasMultiUserSupport) + Provider.value(value: context.read()), + ], + child: Provider( + create: (_) => DocumentSearchCubit( + context.read(), + context.read(), + context.read(), + Hive.box(HiveBoxes.localUserAppState) + .get(LocalUserAccount.current.id)!, + ), + builder: (_, __) => const DocumentSearchPage(), ), - builder: (_, __) => const DocumentSearchPage(), - ), - ); - }, + ); + }, + ), ); } IconButton _buildUserAvatar(BuildContext context) { return IconButton( + padding: const EdgeInsets.all(6), icon: GlobalSettingsBuilder( builder: (context, settings) { return ValueListenableBuilder( diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index 64e3563..39e7e35 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -21,6 +21,7 @@ class SliverSearchBar extends StatelessWidget { Widget build(BuildContext context) { final currentUser = Hive.box(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; + return SliverPersistentHeader( floating: floating, pinned: pinned, diff --git a/lib/features/documents/cubit/documents_cubit.dart b/lib/features/documents/cubit/documents_cubit.dart index 78226a8..edeb82b 100644 --- a/lib/features/documents/cubit/documents_cubit.dart +++ b/lib/features/documents/cubit/documents_cubit.dart @@ -114,8 +114,9 @@ class DocumentsCubit extends HydratedCubit with DocumentPagingBl void setViewType(ViewType viewType) { emit(state.copyWith(viewType: viewType)); - _userState.documentsPageViewType = viewType; - _userState.save(); + _userState + ..documentsPageViewType = viewType + ..save(); } @override diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index fc2c399..847b64e 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -22,6 +22,7 @@ import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:sliver_tools/sliver_tools.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -107,7 +108,7 @@ class _DocumentsPageState extends State with SingleTickerProvider }, builder: (context, connectivityState) { return SafeArea( - top: context.watch().state.selection.isEmpty, + top: true, child: Scaffold( drawer: const AppDrawer(), floatingActionButton: BlocBuilder( @@ -160,13 +161,18 @@ class _DocumentsPageState extends State with SingleTickerProvider handle: searchBarHandle, sliver: BlocBuilder( builder: (context, state) { - if (state.selection.isNotEmpty) { - // Show selection app bar when selection mode is active - return DocumentSelectionSliverAppBar( - state: state, - ); - } - return const SliverSearchBar(floating: true); + return AnimatedSwitcher( + layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder, + transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder, + child: state.selection.isEmpty + ? const SliverSearchBar(floating: true) + : DocumentSelectionSliverAppBar( + state: state, + ), + duration: const Duration( + milliseconds: 250, + ), + ); }, ), ), diff --git a/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart b/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart index 8289761..ba0d9a8 100644 --- a/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart +++ b/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart @@ -35,7 +35,7 @@ class ViewTypeSelectionWidget extends StatelessWidget { ), // Ensures text is not split into two lines position: PopupMenuPosition.under, initialValue: viewType, - icon: Icon(icon), + icon: Icon(icon, color: Theme.of(context).colorScheme.primary), itemBuilder: (context) => [ _buildViewTypeOption( context, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 6bac0f8..967c083 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -10,13 +10,12 @@ import 'package:paperless_api/paperless_api.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/global/constants.dart'; +import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/service/file_description.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart'; -import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; -import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/route_description.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; @@ -139,25 +138,23 @@ class _HomePageState extends State with WidgetsBindingObserver { } return; } + + if (!LocalUserAccount.current.paperlessUser + .hasPermission(PermissionAction.add, PermissionTarget.document)) { + Fluttertoast.showToast( + msg: "You do not have the permissions to upload documents.", + ); + return; + } final fileDescription = FileDescription.fromPath(mediaFile.path); if (await File(mediaFile.path).exists()) { final bytes = File(mediaFile.path).readAsBytesSync(); - final result = await Navigator.push( + final result = await pushDocumentUploadPreparationPage( context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: DocumentUploadCubit( - context.read(), - context.read(), - ), - child: DocumentUploadPreparationPage( - fileBytes: bytes, - filename: fileDescription.filename, - title: fileDescription.filename, - fileExtension: fileDescription.extension, - ), - ), - ), + bytes: bytes, + filename: fileDescription.filename, + title: fileDescription.filename, + fileExtension: fileDescription.extension, ); if (result?.success ?? false) { await Fluttertoast.showToast( diff --git a/lib/features/home/view/route_description.dart b/lib/features/home/view/route_description.dart index 367c41a..8440e03 100644 --- a/lib/features/home/view/route_description.dart +++ b/lib/features/home/view/route_description.dart @@ -5,12 +5,14 @@ class RouteDescription { final Icon icon; final Icon selectedIcon; final Widget Function(Widget icon)? badgeBuilder; + final bool enabled; RouteDescription({ required this.label, required this.icon, required this.selectedIcon, this.badgeBuilder, + this.enabled = true, }); NavigationDestination toNavigationDestination() { diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 6c93123..3d385cc 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -146,7 +146,6 @@ class _LabelsPageState extends State with SingleTickerProviderStateM labels: context.watch().state.correspondents, filterBuilder: (label) => DocumentFilter( correspondent: IdQueryParameter.fromId(label.id!), - pageSize: label.documentCount ?? 0, ), canEdit: LocalUserAccount.current.paperlessUser.hasPermission( PermissionAction.change, PermissionTarget.correspondent), @@ -171,7 +170,6 @@ class _LabelsPageState extends State with SingleTickerProviderStateM labels: context.watch().state.documentTypes, filterBuilder: (label) => DocumentFilter( documentType: IdQueryParameter.fromId(label.id!), - pageSize: label.documentCount ?? 0, ), canEdit: LocalUserAccount.current.paperlessUser.hasPermission( PermissionAction.change, PermissionTarget.documentType), @@ -196,7 +194,6 @@ class _LabelsPageState extends State with SingleTickerProviderStateM labels: context.watch().state.tags, filterBuilder: (label) => DocumentFilter( tags: TagsQuery.ids(include: [label.id!]), - pageSize: label.documentCount ?? 0, ), canEdit: LocalUserAccount.current.paperlessUser .hasPermission(PermissionAction.change, PermissionTarget.tag), @@ -231,7 +228,6 @@ class _LabelsPageState extends State with SingleTickerProviderStateM onEdit: _openEditStoragePathPage, filterBuilder: (label) => DocumentFilter( storagePath: IdQueryParameter.fromId(label.id!), - pageSize: label.documentCount ?? 0, ), canEdit: LocalUserAccount.current.paperlessUser.hasPermission( PermissionAction.change, PermissionTarget.storagePath), diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 9fb94ed..2438689 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -1,13 +1,10 @@ -import 'dart:convert'; -import 'dart:typed_data'; - import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/config/hive/hive_extensions.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/database/tables/local_user_app_state.dart'; @@ -88,39 +85,38 @@ class AuthenticationCubit extends Cubit { return; } } + await withEncryptedBox(HiveBoxes.localUserCredentials, + (credentialsBox) async { + if (!credentialsBox.containsKey(localUserId)) { + await credentialsBox.close(); + debugPrint("Invalid authentication for $localUserId"); + return; + } + final credentials = credentialsBox.get(localUserId); + await _resetExternalState(); - final credentialsBox = await _getUserCredentialsBox(); - if (!credentialsBox.containsKey(localUserId)) { - await credentialsBox.close(); - debugPrint("Invalid authentication for $localUserId"); - return; - } - final credentials = credentialsBox.get(localUserId); - await credentialsBox.close(); + _sessionManager.updateSettings( + authToken: credentials!.token, + clientCertificate: credentials.clientCertificate, + baseUrl: account.serverUrl, + ); - await _resetExternalState(); + globalSettings.currentLoggedInUser = localUserId; + await globalSettings.save(); - _sessionManager.updateSettings( - authToken: credentials!.token, - clientCertificate: credentials.clientCertificate, - baseUrl: account.serverUrl, - ); + final apiVersion = await _getApiVersion(_sessionManager.client); - globalSettings.currentLoggedInUser = localUserId; - await globalSettings.save(); + await _updateRemoteUser( + _sessionManager, + Hive.box(HiveBoxes.localUserAccount).get(localUserId)!, + apiVersion, + ); - final apiVersion = await _getApiVersion(_sessionManager.client); - - await _updateRemoteUser( - _sessionManager, - Hive.box(HiveBoxes.localUserAccount).get(localUserId)!, - apiVersion, - ); - - emit(AuthenticationState.authenticated( - localUserId: localUserId, - apiVersion: apiVersion, - )); + emit(AuthenticationState.authenticated( + localUserId: localUserId, + apiVersion: apiVersion, + )); + }); } Future addAccount({ @@ -146,14 +142,14 @@ class AuthenticationCubit extends Cubit { Future removeAccount(String userId) async { final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; final userAccountBox = Hive.box(HiveBoxes.localUserAccount); - final userCredentialsBox = await _getUserCredentialsBox(); final userAppStateBox = Hive.box(HiveBoxes.localUserAppState); final currentUser = globalSettings.currentLoggedInUser; await userAccountBox.delete(userId); await userAppStateBox.delete(userId); - await userCredentialsBox.delete(userId); - await userCredentialsBox.close(); + await withEncryptedBox(HiveBoxes.localUserCredentials, (box) { + box.delete(userId); + }); if (currentUser == userId) { return logout(); @@ -182,9 +178,10 @@ class AuthenticationCubit extends Cubit { } } - final userCredentialsBox = await _getUserCredentialsBox(); - final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!); - await userCredentialsBox.close(); + final authentication = await withEncryptedBox( + HiveBoxes.localUserCredentials, (box) { + return box.get(globalSettings.currentLoggedInUser!); + }); if (authentication == null) { throw Exception( @@ -218,28 +215,6 @@ class AuthenticationCubit extends Cubit { emit(const AuthenticationState.unauthenticated()); } - Future _getEncryptedBoxKey() async { - const secureStorage = FlutterSecureStorage(); - if (!await secureStorage.containsKey(key: 'key')) { - final key = Hive.generateSecureKey(); - - await secureStorage.write( - key: 'key', - value: base64UrlEncode(key), - ); - } - final key = (await secureStorage.read(key: 'key'))!; - return base64Decode(key); - } - - Future> _getUserCredentialsBox() async { - final keyBytes = await _getEncryptedBoxKey(); - return Hive.openBox( - HiveBoxes.localUserCredentials, - encryptionCipher: HiveAesCipher(keyBytes), - ); - } - Future _resetExternalState() async { _sessionManager.resetSettings(); await HydratedBloc.storage.clear(); @@ -305,15 +280,15 @@ class AuthenticationCubit extends Cubit { ); // Save credentials in encrypted box - final userCredentialsBox = await _getUserCredentialsBox(); - await userCredentialsBox.put( - localUserId, - UserCredentials( - token: token, - clientCertificate: clientCert, - ), - ); - userCredentialsBox.close(); + await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async { + await box.put( + localUserId, + UserCredentials( + token: token, + clientCertificate: clientCert, + ), + ); + }); return serverUser.id; } diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index a7d9f9b..22a3cd3 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -4,6 +4,7 @@ import 'package:paperless_mobile/features/settings/view/pages/application_settin import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -25,10 +26,21 @@ class SettingsPage extends StatelessWidget { textAlign: TextAlign.center, ), subtitle: FutureBuilder( + future: context.read().getServerInformation(), builder: (context, snapshot) { + if (snapshot.hasError) { + return Text( + "Something went wrong while retrieving server data.", //TODO: INTL + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ); + } if (!snapshot.hasData) { return Text( - "Loading server information...", + "Loading server information...", //TODO: INTL style: Theme.of(context).textTheme.labelSmall, textAlign: TextAlign.center, ); @@ -39,7 +51,9 @@ class SettingsPage extends StatelessWidget { ' ' + serverData.version.toString() + ' (API v${serverData.apiVersion})', - style: Theme.of(context).textTheme.labelSmall, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), textAlign: TextAlign.center, ); }, diff --git a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart index e1c8d8f..7bf5185 100644 --- a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart +++ b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart @@ -11,4 +11,20 @@ extension UserPermissionExtension on UserModel { v2: (_) => true, ); } + + bool hasPermissions(List actions, List targets) { + return map( + v3: (user) { + final permissions = [ + for (var action in actions) + for (var target in targets) [action, target].join("_") + ]; + return permissions.every((requestedPermission) => + user.userPermissions.contains(requestedPermission) || + user.inheritedPermissions + .any((element) => element.split(".").last == requestedPermission)); + }, + v2: (_) => true, + ); + } } diff --git a/pubspec.lock b/pubspec.lock index b0a0506..d3d982b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1417,6 +1417,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: "direct main" + description: + name: sliver_tools + sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c + url: "https://pub.dev" + source: hosted + version: "0.2.10" source_gen: dependency: transitive description: @@ -1705,6 +1713,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "5604dac1178680a34fbe4a08c7b69ec42cca6601dc300009ec9ff69bef284cc2" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "57a22c86065375c1598b57224f92d6008141be0c877c64100de8bfb6f71083d8" + url: "https://pub.dev" + source: hosted + version: "3.7.1" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "6bbc6ade302b842999b27cbaa7171241c273deea8a9c73f92ceb3d811c767de2" + url: "https://pub.dev" + source: hosted + version: "3.4.4" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f5d7080..8285c4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,6 +92,8 @@ dependencies: animations: ^2.0.7 hive_flutter: ^1.1.0 flutter_secure_storage: ^8.0.0 + sliver_tools: ^0.2.10 + webview_flutter: ^4.2.1 dependency_overrides: intl: ^0.18.0