mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 15:15:50 -06:00
feat: Add permission checks, fix search, fix document upload, fix linked documents always being loaded all at once instead of paged
This commit is contained in:
35
lib/core/config/hive/hive_extensions.dart
Normal file
35
lib/core/config/hive/hive_extensions.dart
Normal file
@@ -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<R?> withEncryptedBox<T, R>(String name, FutureOr<R?> Function(Box<T> box) callback) async {
|
||||||
|
final key = await _getEncryptedBoxKey();
|
||||||
|
final box = await Hive.openBox<T>(
|
||||||
|
name,
|
||||||
|
encryptionCipher: HiveAesCipher(key),
|
||||||
|
);
|
||||||
|
final result = await callback(box);
|
||||||
|
await box.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List> _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);
|
||||||
|
}
|
||||||
@@ -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/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_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_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/cubit/document_search_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.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';
|
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||||
@@ -38,6 +37,7 @@ import 'package:provider/provider.dart';
|
|||||||
Future<void> pushDocumentSearchPage(BuildContext context) {
|
Future<void> pushDocumentSearchPage(BuildContext context) {
|
||||||
final currentUser =
|
final currentUser =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
||||||
|
final userRepo = context.read<UserRepository>();
|
||||||
return Navigator.of(context).push(
|
return Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => MultiProvider(
|
builder: (_) => MultiProvider(
|
||||||
@@ -46,6 +46,7 @@ Future<void> pushDocumentSearchPage(BuildContext context) {
|
|||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
Provider.value(value: context.read<CacheManager>()),
|
||||||
|
Provider.value(value: userRepo),
|
||||||
],
|
],
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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/constants.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
@@ -92,8 +93,11 @@ class AppDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => Provider.value(
|
builder: (_) => MultiProvider(
|
||||||
value: context.read<ApiVersion>(),
|
providers: [
|
||||||
|
Provider.value(value: context.read<PaperlessServerStatsApi>()),
|
||||||
|
Provider.value(value: context.read<ApiVersion>()),
|
||||||
|
],
|
||||||
child: const SettingsPage(),
|
child: const SettingsPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -240,7 +240,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
SliverOverlapInjector(
|
SliverOverlapInjector(
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
),
|
),
|
||||||
const DocumentPermissionsWidget(),
|
DocumentPermissionsWidget(
|
||||||
|
document: state.document,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
class DocumentPermissionsWidget extends StatelessWidget {
|
class DocumentPermissionsWidget extends StatefulWidget {
|
||||||
const DocumentPermissionsWidget({super.key});
|
final DocumentModel document;
|
||||||
|
const DocumentPermissionsWidget({super.key, required this.document});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DocumentPermissionsWidget> createState() => _DocumentPermissionsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
|
|||||||
@@ -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/cubit/document_scanner_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.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_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/documents/view/pages/document_view.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.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/generated/l10n/app_localizations.dart';
|
||||||
|
|||||||
@@ -27,88 +27,89 @@ class DocumentSearchBar extends StatefulWidget {
|
|||||||
class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return OpenContainer(
|
return Container(
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
margin: EdgeInsets.only(top: 8),
|
||||||
transitionType: ContainerTransitionType.fadeThrough,
|
child: OpenContainer(
|
||||||
closedElevation: 1,
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
middleColor: Theme.of(context).colorScheme.surfaceVariant,
|
transitionType: ContainerTransitionType.fadeThrough,
|
||||||
openColor: Theme.of(context).colorScheme.background,
|
closedElevation: 1,
|
||||||
closedColor: Theme.of(context).colorScheme.surfaceVariant,
|
middleColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
closedShape: RoundedRectangleBorder(
|
openColor: Theme.of(context).colorScheme.background,
|
||||||
borderRadius: BorderRadius.circular(56),
|
closedColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
),
|
closedShape: RoundedRectangleBorder(
|
||||||
closedBuilder: (_, action) {
|
borderRadius: BorderRadius.circular(56),
|
||||||
return InkWell(
|
),
|
||||||
child: ConstrainedBox(
|
closedBuilder: (_, action) {
|
||||||
constraints: const BoxConstraints(
|
return InkWell(
|
||||||
maxWidth: 720,
|
onTap: action,
|
||||||
minWidth: 360,
|
child: ConstrainedBox(
|
||||||
maxHeight: 56,
|
constraints: const BoxConstraints(
|
||||||
minHeight: 48,
|
maxWidth: 720,
|
||||||
),
|
minWidth: 360,
|
||||||
child: Row(
|
maxHeight: 56,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
minHeight: 48,
|
||||||
children: [
|
),
|
||||||
Flexible(
|
child: Row(
|
||||||
child: Padding(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
children: [
|
||||||
child: Row(
|
Flexible(
|
||||||
children: [
|
child: Padding(
|
||||||
IconButton(
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
icon: Icon(Icons.menu),
|
child: Row(
|
||||||
onPressed: Scaffold.of(context).openDrawer,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
Expanded(
|
IconButton(
|
||||||
child: Hero(
|
icon: const Icon(Icons.menu),
|
||||||
tag: "search_hero_tag",
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
child: TextField(
|
),
|
||||||
enabled: false,
|
Flexible(
|
||||||
decoration: InputDecoration(
|
child: Text(
|
||||||
border: InputBorder.none,
|
S.of(context)!.searchDocuments,
|
||||||
hintText: S.of(context)!.searchDocuments,
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
hintStyle: TextStyle(
|
fontWeight: FontWeight.w500,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
_buildUserAvatar(context),
|
||||||
_buildUserAvatar(context),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
openBuilder: (_, action) {
|
||||||
openBuilder: (_, action) {
|
return MultiProvider(
|
||||||
return MultiProvider(
|
providers: [
|
||||||
providers: [
|
Provider.value(value: context.read<LabelRepository>()),
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
Provider.value(value: context.read<CacheManager>()),
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
Provider.value(value: context.read<ApiVersion>()),
|
||||||
Provider.value(value: context.read<ApiVersion>()),
|
if (context.read<ApiVersion>().hasMultiUserSupport)
|
||||||
Provider.value(value: context.read<UserRepository>()),
|
Provider.value(value: context.read<UserRepository>()),
|
||||||
],
|
],
|
||||||
child: Provider(
|
child: Provider(
|
||||||
create: (_) => DocumentSearchCubit(
|
create: (_) => DocumentSearchCubit(
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||||
.get(LocalUserAccount.current.id)!,
|
.get(LocalUserAccount.current.id)!,
|
||||||
|
),
|
||||||
|
builder: (_, __) => const DocumentSearchPage(),
|
||||||
),
|
),
|
||||||
builder: (_, __) => const DocumentSearchPage(),
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton _buildUserAvatar(BuildContext context) {
|
IconButton _buildUserAvatar(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
icon: GlobalSettingsBuilder(
|
icon: GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentUser =
|
final currentUser =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
||||||
|
|
||||||
return SliverPersistentHeader(
|
return SliverPersistentHeader(
|
||||||
floating: floating,
|
floating: floating,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
|
|||||||
@@ -114,8 +114,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
|
|||||||
|
|
||||||
void setViewType(ViewType viewType) {
|
void setViewType(ViewType viewType) {
|
||||||
emit(state.copyWith(viewType: viewType));
|
emit(state.copyWith(viewType: viewType));
|
||||||
_userState.documentsPageViewType = viewType;
|
_userState
|
||||||
_userState.save();
|
..documentsPageViewType = viewType
|
||||||
|
..save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -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/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
|
||||||
class DocumentFilterIntent {
|
class DocumentFilterIntent {
|
||||||
final DocumentFilter? filter;
|
final DocumentFilter? filter;
|
||||||
@@ -107,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
|
|||||||
},
|
},
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
top: context.watch<DocumentsCubit>().state.selection.isEmpty,
|
top: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
@@ -160,13 +161,18 @@ class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProvider
|
|||||||
handle: searchBarHandle,
|
handle: searchBarHandle,
|
||||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.selection.isNotEmpty) {
|
return AnimatedSwitcher(
|
||||||
// Show selection app bar when selection mode is active
|
layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder,
|
||||||
return DocumentSelectionSliverAppBar(
|
transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder,
|
||||||
state: state,
|
child: state.selection.isEmpty
|
||||||
);
|
? const SliverSearchBar(floating: true)
|
||||||
}
|
: DocumentSelectionSliverAppBar(
|
||||||
return const SliverSearchBar(floating: true);
|
state: state,
|
||||||
|
),
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 250,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
|||||||
), // Ensures text is not split into two lines
|
), // Ensures text is not split into two lines
|
||||||
position: PopupMenuPosition.under,
|
position: PopupMenuPosition.under,
|
||||||
initialValue: viewType,
|
initialValue: viewType,
|
||||||
icon: Icon(icon),
|
icon: Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
_buildViewTypeOption(
|
_buildViewTypeOption(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -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/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_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/service/file_description.dart';
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.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_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/documents/view/pages/documents_page.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/route_description.dart';
|
import 'package:paperless_mobile/features/home/view/route_description.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||||
@@ -139,25 +138,23 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
return;
|
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);
|
final fileDescription = FileDescription.fromPath(mediaFile.path);
|
||||||
if (await File(mediaFile.path).exists()) {
|
if (await File(mediaFile.path).exists()) {
|
||||||
final bytes = File(mediaFile.path).readAsBytesSync();
|
final bytes = File(mediaFile.path).readAsBytesSync();
|
||||||
final result = await Navigator.push<DocumentUploadResult>(
|
final result = await pushDocumentUploadPreparationPage(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
bytes: bytes,
|
||||||
builder: (context) => BlocProvider.value(
|
filename: fileDescription.filename,
|
||||||
value: DocumentUploadCubit(
|
title: fileDescription.filename,
|
||||||
context.read(),
|
fileExtension: fileDescription.extension,
|
||||||
context.read(),
|
|
||||||
),
|
|
||||||
child: DocumentUploadPreparationPage(
|
|
||||||
fileBytes: bytes,
|
|
||||||
filename: fileDescription.filename,
|
|
||||||
title: fileDescription.filename,
|
|
||||||
fileExtension: fileDescription.extension,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (result?.success ?? false) {
|
if (result?.success ?? false) {
|
||||||
await Fluttertoast.showToast(
|
await Fluttertoast.showToast(
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ class RouteDescription {
|
|||||||
final Icon icon;
|
final Icon icon;
|
||||||
final Icon selectedIcon;
|
final Icon selectedIcon;
|
||||||
final Widget Function(Widget icon)? badgeBuilder;
|
final Widget Function(Widget icon)? badgeBuilder;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
RouteDescription({
|
RouteDescription({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.selectedIcon,
|
required this.selectedIcon,
|
||||||
this.badgeBuilder,
|
this.badgeBuilder,
|
||||||
|
this.enabled = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
NavigationDestination toNavigationDestination() {
|
NavigationDestination toNavigationDestination() {
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
labels: context.watch<LabelCubit>().state.correspondents,
|
labels: context.watch<LabelCubit>().state.correspondents,
|
||||||
filterBuilder: (label) => DocumentFilter(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
correspondent: IdQueryParameter.fromId(label.id!),
|
correspondent: IdQueryParameter.fromId(label.id!),
|
||||||
pageSize: label.documentCount ?? 0,
|
|
||||||
),
|
),
|
||||||
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
|
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||||
PermissionAction.change, PermissionTarget.correspondent),
|
PermissionAction.change, PermissionTarget.correspondent),
|
||||||
@@ -171,7 +170,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
labels: context.watch<LabelCubit>().state.documentTypes,
|
labels: context.watch<LabelCubit>().state.documentTypes,
|
||||||
filterBuilder: (label) => DocumentFilter(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
documentType: IdQueryParameter.fromId(label.id!),
|
documentType: IdQueryParameter.fromId(label.id!),
|
||||||
pageSize: label.documentCount ?? 0,
|
|
||||||
),
|
),
|
||||||
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
|
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||||
PermissionAction.change, PermissionTarget.documentType),
|
PermissionAction.change, PermissionTarget.documentType),
|
||||||
@@ -196,7 +194,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
labels: context.watch<LabelCubit>().state.tags,
|
labels: context.watch<LabelCubit>().state.tags,
|
||||||
filterBuilder: (label) => DocumentFilter(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
tags: TagsQuery.ids(include: [label.id!]),
|
tags: TagsQuery.ids(include: [label.id!]),
|
||||||
pageSize: label.documentCount ?? 0,
|
|
||||||
),
|
),
|
||||||
canEdit: LocalUserAccount.current.paperlessUser
|
canEdit: LocalUserAccount.current.paperlessUser
|
||||||
.hasPermission(PermissionAction.change, PermissionTarget.tag),
|
.hasPermission(PermissionAction.change, PermissionTarget.tag),
|
||||||
@@ -231,7 +228,6 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
onEdit: _openEditStoragePathPage,
|
onEdit: _openEditStoragePathPage,
|
||||||
filterBuilder: (label) => DocumentFilter(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
storagePath: IdQueryParameter.fromId(label.id!),
|
storagePath: IdQueryParameter.fromId(label.id!),
|
||||||
pageSize: label.documentCount ?? 0,
|
|
||||||
),
|
),
|
||||||
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
|
canEdit: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||||
PermissionAction.change, PermissionTarget.storagePath),
|
PermissionAction.change, PermissionTarget.storagePath),
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.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_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/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
@@ -88,39 +85,38 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await withEncryptedBox<UserCredentials, void>(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();
|
_sessionManager.updateSettings(
|
||||||
if (!credentialsBox.containsKey(localUserId)) {
|
authToken: credentials!.token,
|
||||||
await credentialsBox.close();
|
clientCertificate: credentials.clientCertificate,
|
||||||
debugPrint("Invalid authentication for $localUserId");
|
baseUrl: account.serverUrl,
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
final credentials = credentialsBox.get(localUserId);
|
|
||||||
await credentialsBox.close();
|
|
||||||
|
|
||||||
await _resetExternalState();
|
globalSettings.currentLoggedInUser = localUserId;
|
||||||
|
await globalSettings.save();
|
||||||
|
|
||||||
_sessionManager.updateSettings(
|
final apiVersion = await _getApiVersion(_sessionManager.client);
|
||||||
authToken: credentials!.token,
|
|
||||||
clientCertificate: credentials.clientCertificate,
|
|
||||||
baseUrl: account.serverUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
globalSettings.currentLoggedInUser = localUserId;
|
await _updateRemoteUser(
|
||||||
await globalSettings.save();
|
_sessionManager,
|
||||||
|
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
|
||||||
|
apiVersion,
|
||||||
|
);
|
||||||
|
|
||||||
final apiVersion = await _getApiVersion(_sessionManager.client);
|
emit(AuthenticationState.authenticated(
|
||||||
|
localUserId: localUserId,
|
||||||
await _updateRemoteUser(
|
apiVersion: apiVersion,
|
||||||
_sessionManager,
|
));
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
|
});
|
||||||
apiVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
emit(AuthenticationState.authenticated(
|
|
||||||
localUserId: localUserId,
|
|
||||||
apiVersion: apiVersion,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> addAccount({
|
Future<String> addAccount({
|
||||||
@@ -146,14 +142,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
Future<void> removeAccount(String userId) async {
|
Future<void> removeAccount(String userId) async {
|
||||||
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||||
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||||
final userCredentialsBox = await _getUserCredentialsBox();
|
|
||||||
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||||
final currentUser = globalSettings.currentLoggedInUser;
|
final currentUser = globalSettings.currentLoggedInUser;
|
||||||
|
|
||||||
await userAccountBox.delete(userId);
|
await userAccountBox.delete(userId);
|
||||||
await userAppStateBox.delete(userId);
|
await userAppStateBox.delete(userId);
|
||||||
await userCredentialsBox.delete(userId);
|
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) {
|
||||||
await userCredentialsBox.close();
|
box.delete(userId);
|
||||||
|
});
|
||||||
|
|
||||||
if (currentUser == userId) {
|
if (currentUser == userId) {
|
||||||
return logout();
|
return logout();
|
||||||
@@ -182,9 +178,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final userCredentialsBox = await _getUserCredentialsBox();
|
final authentication = await withEncryptedBox<UserCredentials, UserCredentials>(
|
||||||
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!);
|
HiveBoxes.localUserCredentials, (box) {
|
||||||
await userCredentialsBox.close();
|
return box.get(globalSettings.currentLoggedInUser!);
|
||||||
|
});
|
||||||
|
|
||||||
if (authentication == null) {
|
if (authentication == null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
@@ -218,28 +215,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
emit(const AuthenticationState.unauthenticated());
|
emit(const AuthenticationState.unauthenticated());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> _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<Box<UserCredentials>> _getUserCredentialsBox() async {
|
|
||||||
final keyBytes = await _getEncryptedBoxKey();
|
|
||||||
return Hive.openBox<UserCredentials>(
|
|
||||||
HiveBoxes.localUserCredentials,
|
|
||||||
encryptionCipher: HiveAesCipher(keyBytes),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _resetExternalState() async {
|
Future<void> _resetExternalState() async {
|
||||||
_sessionManager.resetSettings();
|
_sessionManager.resetSettings();
|
||||||
await HydratedBloc.storage.clear();
|
await HydratedBloc.storage.clear();
|
||||||
@@ -305,15 +280,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Save credentials in encrypted box
|
// Save credentials in encrypted box
|
||||||
final userCredentialsBox = await _getUserCredentialsBox();
|
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
|
||||||
await userCredentialsBox.put(
|
await box.put(
|
||||||
localUserId,
|
localUserId,
|
||||||
UserCredentials(
|
UserCredentials(
|
||||||
token: token,
|
token: token,
|
||||||
clientCertificate: clientCert,
|
clientCertificate: clientCert,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
userCredentialsBox.close();
|
});
|
||||||
return serverUser.id;
|
return serverUser.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/pages/security_settings_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
@@ -25,10 +26,21 @@ class SettingsPage extends StatelessWidget {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
subtitle: FutureBuilder<PaperlessServerInformationModel>(
|
subtitle: FutureBuilder<PaperlessServerInformationModel>(
|
||||||
|
future: context.read<PaperlessServerStatsApi>().getServerInformation(),
|
||||||
builder: (context, snapshot) {
|
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) {
|
if (!snapshot.hasData) {
|
||||||
return Text(
|
return Text(
|
||||||
"Loading server information...",
|
"Loading server information...", //TODO: INTL
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
);
|
);
|
||||||
@@ -39,7 +51,9 @@ class SettingsPage extends StatelessWidget {
|
|||||||
' ' +
|
' ' +
|
||||||
serverData.version.toString() +
|
serverData.version.toString() +
|
||||||
' (API v${serverData.apiVersion})',
|
' (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,
|
textAlign: TextAlign.center,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,4 +11,20 @@ extension UserPermissionExtension on UserModel {
|
|||||||
v2: (_) => true,
|
v2: (_) => true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasPermissions(List<PermissionAction> actions, List<PermissionTarget> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
pubspec.lock
40
pubspec.lock
@@ -1417,6 +1417,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
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:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1705,6 +1713,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
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:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ dependencies:
|
|||||||
animations: ^2.0.7
|
animations: ^2.0.7
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
flutter_secure_storage: ^8.0.0
|
flutter_secure_storage: ^8.0.0
|
||||||
|
sliver_tools: ^0.2.10
|
||||||
|
webview_flutter: ^4.2.1
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
intl: ^0.18.0
|
intl: ^0.18.0
|
||||||
|
|||||||
Reference in New Issue
Block a user