feat: Add more user related state to hive

This commit is contained in:
Anton Stubenbord
2023-04-23 16:48:11 +02:00
parent 1b9e4fbb81
commit 5c0ef7f853
32 changed files with 408 additions and 272 deletions

View File

@@ -70,7 +70,7 @@ android {
} }
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.debug
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/custpm_adapters/theme_mode_adapter.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/custom_adapters/theme_mode_adapter.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart'; import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart'; import 'package:paperless_mobile/features/login/model/user_account.dart';
@@ -38,6 +39,7 @@ void registerHiveAdapters() {
Hive.registerAdapter(UserSettingsAdapter()); Hive.registerAdapter(UserSettingsAdapter());
Hive.registerAdapter(UserCredentialsAdapter()); Hive.registerAdapter(UserCredentialsAdapter());
Hive.registerAdapter(UserAccountAdapter()); Hive.registerAdapter(UserAccountAdapter());
Hive.registerAdapter(DocumentFilterAdapter());
} }
extension HiveSingleValueBox<T> on Box<T> { extension HiveSingleValueBox<T> on Box<T> {

View File

@@ -3,13 +3,14 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'document_search_state.dart'; part 'document_search_state.dart';
part 'document_search_cubit.g.dart'; part 'document_search_cubit.g.dart';
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState> class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
@@ -20,7 +21,6 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
@override @override
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
DocumentSearchCubit(this.api, this.notifier, this._labelRepository) DocumentSearchCubit(this.api, this.notifier, this._labelRepository)
: super(const DocumentSearchState()) { : super(const DocumentSearchState()) {
_labelRepository.addListener( _labelRepository.addListener(
@@ -119,4 +119,8 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
Map<String, dynamic>? toJson(DocumentSearchState state) { Map<String, dynamic>? toJson(DocumentSearchState state) {
return state.toJson(); return state.toJson();
} }
@override
// TODO: implement account
UserAccount get account => throw UnimplementedError();
} }

View File

@@ -24,7 +24,7 @@ class DocumentSearchState extends DocumentPagingState {
this.searchHistory = const [], this.searchHistory = const [],
this.suggestions = const [], this.suggestions = const [],
this.viewType = ViewType.detailed, this.viewType = ViewType.detailed,
super.filter, super.filter = const DocumentFilter(),
super.hasLoaded, super.hasLoaded,
super.isLoading, super.isLoading,
super.value, super.value,

View File

@@ -5,7 +5,8 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.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/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
as s;
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/login/model/user_account.dart'; import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
@@ -45,10 +46,14 @@ class SliverSearchBar extends StatelessWidget {
icon: GlobalSettingsBuilder( icon: GlobalSettingsBuilder(
builder: (context, settings) { builder: (context, settings) {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(), valueListenable:
Hive.box<UserAccount>(HiveBoxes.userAccount)
.listenable(),
builder: (context, box, _) { builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!; final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(userId: settings.currentLoggedInUser!, account: account); return UserAvatar(
userId: settings.currentLoggedInUser!,
account: account);
}, },
); );
}, },

View File

@@ -6,6 +6,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -23,8 +24,15 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
@override @override
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
DocumentsCubit(this.api, this.notifier, this._labelRepository) @override
: super(const DocumentsState()) { final UserAccount account;
DocumentsCubit(
this.api,
this.notifier,
this._labelRepository,
this.account,
) : super(DocumentsState(filter: account.settings.currentDocumentFilter)) {
notifier.addListener( notifier.addListener(
this, this,
onUpdated: (document) { onUpdated: (document) {

View File

@@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart'; 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/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.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';
@@ -26,6 +28,7 @@ import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
@@ -230,7 +233,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
return icon; return icon;
}, },
)), ),
),
]; ];
final routes = <Widget>[ final routes = <Widget>[
MultiBlocProvider( MultiBlocProvider(
@@ -241,6 +245,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!,
)..reload(), )..reload(),
), ),
BlocProvider( BlocProvider(
@@ -275,7 +280,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
listeners: [ listeners: [
BlocListener<ConnectivityCubit, ConnectivityState>( BlocListener<ConnectivityCubit, ConnectivityState>(
//Only re-initialize data if the connectivity changed from not connected to connected //Only re-initialize data if the connectivity changed from not connected to connected
listenWhen: (previous, current) => current == ConnectivityState.connected, listenWhen: (previous, current) =>
current == ConnectivityState.connected,
listener: (context, state) { listener: (context, state) {
_initializeData(context); _initializeData(context);
}, },
@@ -284,7 +290,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
listener: (context, state) { listener: (context, state) {
if (state.task != null) { if (state.task != null) {
// Handle local notifications on task change (only when app is running for now). // Handle local notifications on task change (only when app is running for now).
context.read<LocalNotificationService>().notifyTaskChanged(state.task!); context
.read<LocalNotificationService>()
.notifyTaskChanged(state.task!);
} }
}, },
), ),
@@ -297,7 +305,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
children: [ children: [
NavigationRail( NavigationRail(
labelType: NavigationRailLabelType.all, labelType: NavigationRailLabelType.all,
destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(), destinations: destinations
.map((e) => e.toNavigationRailDestination())
.toList(),
selectedIndex: _currentIndex, selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged, onDestinationSelected: _onNavigationChanged,
), ),
@@ -315,7 +325,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
elevation: 4.0, elevation: 4.0,
selectedIndex: _currentIndex, selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged, onDestinationSelected: _onNavigationChanged,
destinations: destinations.map((e) => e.toNavigationDestination()).toList(), destinations:
destinations.map((e) => e.toNavigationDestination()).toList(),
), ),
body: routes[_currentIndex], body: routes[_currentIndex],
); );

View File

@@ -24,7 +24,7 @@ class VerifyIdentityPage extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
title: Text(S.of(context)!.verifyYourIdentity), title: Text(S.of(context)!.verifyYourIdentity),
), ),
body: UserSettingsBuilder( body: UserAccountBuilder(
builder: (context, settings) { builder: (context, settings) {
if (settings == null) { if (settings == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -32,7 +32,9 @@ class VerifyIdentityPage extends StatelessWidget {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate) Text(S
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16), .paddedSymmetrically(horizontal: 16),
const Icon( const Icon(
Icons.fingerprint, Icons.fingerprint,
@@ -54,7 +56,9 @@ class VerifyIdentityPage extends StatelessWidget {
), ),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(), onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(),
child: Text(S.of(context)!.verifyIdentity), child: Text(S.of(context)!.verifyIdentity),
), ),
], ],

View File

@@ -13,7 +13,8 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/document_pag
part 'inbox_cubit.g.dart'; part 'inbox_cubit.g.dart';
part 'inbox_state.dart'; part 'inbox_state.dart';
class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin { class InboxCubit extends HydratedCubit<InboxState>
with DocumentPagingBlocMixin {
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
final PaperlessDocumentsApi _documentsApi; final PaperlessDocumentsApi _documentsApi;
@@ -31,12 +32,17 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
this._statsApi, this._statsApi,
this._labelRepository, this._labelRepository,
this.notifier, this.notifier,
) : super(InboxState(labels: _labelRepository.state)) { ) : super(InboxState(
labels: _labelRepository.state,
)) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
onUpdated: (document) { onUpdated: (document) {
if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) { if (document.tags
.toSet()
.intersection(state.inboxTags.toSet())
.isEmpty) {
remove(document); remove(document);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
} else { } else {
@@ -135,7 +141,8 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
/// from the inbox. /// from the inbox.
/// ///
Future<Iterable<int>> removeFromInbox(DocumentModel document) async { Future<Iterable<int>> removeFromInbox(DocumentModel document) async {
final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet()); final tagsToRemove =
document.tags.toSet().intersection(state.inboxTags.toSet());
final updatedTags = {...document.tags}..removeAll(tagsToRemove); final updatedTags = {...document.tags}..removeAll(tagsToRemove);
final updatedDocument = await api.update( final updatedDocument = await api.update(
@@ -189,8 +196,8 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
Future<void> assignAsn(DocumentModel document) async { Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) { if (document.archiveSerialNumber == null) {
final int asn = await _documentsApi.findNextAsn(); final int asn = await _documentsApi.findNextAsn();
final updatedDocument = final updatedDocument = await _documentsApi
await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn)); .update(document.copyWith(archiveSerialNumber: () => asn));
replace(updatedDocument); replace(updatedDocument);
} }

View File

@@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart'; import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/format_helpers.dart';
class LabelItem<T extends Label> extends StatelessWidget { class LabelItem<T extends Label> extends StatelessWidget {
@@ -42,6 +46,10 @@ class LabelItem<T extends Label> extends StatelessWidget {
onPressed: (label.documentCount ?? 0) == 0 onPressed: (label.documentCount ?? 0) == 0
? null ? null
: () { : () {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
final filter = filterBuilder(label); final filter = filterBuilder(label);
Navigator.push( Navigator.push(
context, context,
@@ -52,6 +60,8 @@ class LabelItem<T extends Label> extends StatelessWidget {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
Hive.box<UserAccount>(HiveBoxes.userAccount)
.get(currentUser)!,
), ),
child: const LinkedDocumentsPage(), child: const LinkedDocumentsPage(),
), ),

View File

@@ -3,6 +3,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -20,12 +21,16 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
@override
// TODO: implement account
final UserAccount account;
LinkedDocumentsCubit( LinkedDocumentsCubit(
DocumentFilter filter, DocumentFilter filter,
this.api, this.api,
this.notifier, this.notifier,
this._labelRepository, this._labelRepository,
) : super(const LinkedDocumentsState()) { this.account,
) : super(LinkedDocumentsState(filter: filter)) {
updateFilter(filter: filter); updateFilter(filter: filter);
_labelRepository.addListener( _labelRepository.addListener(
this, this,

View File

@@ -12,7 +12,7 @@ class LinkedDocumentsState extends DocumentPagingState {
const LinkedDocumentsState({ const LinkedDocumentsState({
this.viewType = ViewType.list, this.viewType = ViewType.list,
super.filter, super.filter = const DocumentFilter(),
super.isLoading, super.isLoading,
super.hasLoaded, super.hasLoaded,
super.value, super.value,

View File

@@ -62,17 +62,17 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final userId = "${credentials.username}@$serverUrl"; final userId = "${credentials.username}@$serverUrl";
// If it is first time login, create settings for this user. // If it is first time login, create settings for this user.
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount); final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
if (!userSettingsBox.containsKey(userId)) {
userSettingsBox.put(userId, UserSettings());
}
final fullName = await _fetchFullName();
final fullName = await _fetchFullName();
if (!userAccountBox.containsKey(userId)) { if (!userAccountBox.containsKey(userId)) {
userAccountBox.put( userAccountBox.put(
userId, userId,
UserAccount( UserAccount(
id: userId,
settings: UserSettings(
currentDocumentFilter: DocumentFilter(),
),
serverUrl: serverUrl, serverUrl: serverUrl,
username: credentials.username!, username: credentials.username!,
fullName: fullName, fullName: fullName,
@@ -81,7 +81,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
// Mark logged in user as currently active user. // Mark logged in user as currently active user.
final globalSettings = GlobalSettings.boxedValue; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = userId; globalSettings.currentLoggedInUser = userId;
globalSettings.save(); globalSettings.save();
@@ -108,26 +109,25 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Switches to another account if it exists. /// Switches to another account if it exists.
Future<void> switchAccount(String userId) async { Future<void> switchAccount(String userId) async {
final globalSettings = GlobalSettings.boxedValue; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.currentLoggedInUser == userId) { if (globalSettings.currentLoggedInUser == userId) {
return; return;
} }
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount); final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
if (!userSettingsBox.containsKey(userId)) { if (!userAccountBox.containsKey(userId)) {
debugPrint("User $userId not yet registered."); debugPrint("User $userId not yet registered.");
return; return;
} }
final userSettings = userSettingsBox.get(userId)!;
final account = userAccountBox.get(userId)!; final account = userAccountBox.get(userId)!;
if (userSettings.isBiometricAuthenticationEnabled) { if (account.settings.isBiometricAuthenticationEnabled) {
final authenticated = final authenticated = await _localAuthService
await _localAuthService.authenticateLocalUser("Authenticate to switch your account."); .authenticateLocalUser("Authenticate to switch your account.");
if (!authenticated) { if (!authenticated) {
debugPrint("User unable to authenticate."); debugPrint("User not authenticated.");
return; return;
} }
} }
@@ -172,7 +172,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final userId = "${credentials.username}@$serverUrl"; final userId = "${credentials.username}@$serverUrl";
final userAccountsBox = Hive.box<UserAccount>(HiveBoxes.userAccount); final userAccountsBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
if (userAccountsBox.containsKey(userId)) { if (userAccountsBox.containsKey(userId)) {
throw Exception("User already exists"); throw Exception("User already exists");
@@ -192,18 +191,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
password: credentials.password!, password: credentials.password!,
); );
sessionManager.resetSettings(); sessionManager.resetSettings();
await userSettingsBox.put(
userId,
UserSettings(
isBiometricAuthenticationEnabled: enableBiometricAuthentication,
),
);
final fullName = await _fetchFullName(); final fullName = await _fetchFullName();
await userAccountsBox.put( await userAccountsBox.put(
userId, userId,
UserAccount( UserAccount(
id: userId,
serverUrl: serverUrl, serverUrl: serverUrl,
username: credentials.username!, username: credentials.username!,
settings: UserSettings(
isBiometricAuthenticationEnabled: enableBiometricAuthentication,
currentDocumentFilter: DocumentFilter(),
),
fullName: fullName, fullName: fullName,
), ),
); );
@@ -221,15 +221,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
Future<void> removeAccount(String userId) async { Future<void> removeAccount(String userId) async {
final globalSettings = GlobalSettings.boxedValue; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final currentUser = globalSettings.currentLoggedInUser; final currentUser = globalSettings.currentLoggedInUser;
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount); final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
final userCredentialsBox = await _getUserCredentialsBox(); final userCredentialsBox = await _getUserCredentialsBox();
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
await userAccountBox.delete(userId); await userAccountBox.delete(userId);
await userCredentialsBox.delete(userId); await userCredentialsBox.delete(userId);
await userSettingsBox.delete(userId);
if (currentUser == userId) { if (currentUser == userId) {
return logout(); return logout();
@@ -240,27 +239,30 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Performs a conditional hydration based on the local authentication success. /// Performs a conditional hydration based on the local authentication success.
/// ///
Future<void> restoreSessionState() async { Future<void> restoreSessionState() async {
final globalSettings = GlobalSettings.boxedValue; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final userId = globalSettings.currentLoggedInUser; final userId = globalSettings.currentLoggedInUser;
if (userId == null) { if (userId == null) {
// If there is nothing to restore, we can quit here. // If there is nothing to restore, we can quit here.
return; return;
} }
final userSettings = Hive.box<UserSettings>(HiveBoxes.userSettings).get(userId)!; final userAccount =
final userAccount = Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!; Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!;
if (userSettings.isBiometricAuthenticationEnabled) { if (userAccount.settings.isBiometricAuthenticationEnabled) {
final localAuthSuccess = final localAuthSuccess = await _localAuthService
await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL .authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (!localAuthSuccess) { if (!localAuthSuccess) {
emit(const AuthenticationState(showBiometricAuthenticationScreen: true)); emit(
const AuthenticationState(showBiometricAuthenticationScreen: true));
return; return;
} }
} }
final userCredentialsBox = await _getUserCredentialsBox(); final userCredentialsBox = await _getUserCredentialsBox();
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!); final authentication =
userCredentialsBox.get(globalSettings.currentLoggedInUser!);
if (authentication != null) { if (authentication != null) {
_dioWrapper.updateSettings( _dioWrapper.updateSettings(
clientCertificate: authentication.clientCertificate, clientCertificate: authentication.clientCertificate,
@@ -276,13 +278,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
), ),
); );
} else { } else {
throw Exception("User should be authenticated but no authentication information was found."); throw Exception(
"User should be authenticated but no authentication information was found.");
} }
} }
Future<void> logout() async { Future<void> logout() async {
await _resetExternalState(); await _resetExternalState();
final globalSettings = GlobalSettings.boxedValue; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings globalSettings
..currentLoggedInUser = null ..currentLoggedInUser = null
..save(); ..save();

View File

@@ -1,20 +1,31 @@
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.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/features/settings/model/user_settings.dart';
part 'user_account.g.dart'; part 'user_account.g.dart';
@HiveType(typeId: HiveTypeIds.userAccount) @HiveType(typeId: HiveTypeIds.userAccount)
class UserAccount { class UserAccount extends HiveObject {
@HiveField(0) @HiveField(0)
final String serverUrl; final String serverUrl;
@HiveField(1) @HiveField(1)
final String username; final String username;
@HiveField(2) @HiveField(2)
final String? fullName; final String? fullName;
@HiveField(3)
final String id;
@HiveField(4)
UserSettings settings;
UserAccount({ UserAccount({
required this.id,
required this.serverUrl, required this.serverUrl,
required this.username, required this.username,
required this.settings,
this.fullName, this.fullName,
}); });
} }

View File

@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'paged_documents_state.dart'; import 'paged_documents_state.dart';
@@ -13,6 +14,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
on BlocBase<State> { on BlocBase<State> {
PaperlessDocumentsApi get api; PaperlessDocumentsApi get api;
DocumentChangedNotifier get notifier; DocumentChangedNotifier get notifier;
UserAccount get account;
Future<void> loadMore() async { Future<void> loadMore() async {
if (state.isLastPageLoaded) { if (state.isLastPageLoaded) {
@@ -28,6 +30,8 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
value: [...state.value, result], value: [...state.value, result],
)); ));
} finally { } finally {
account.settings.currentDocumentFilter = newFilter;
account.save();
emit(state.copyWithPaged(isLoading: false)); emit(state.copyWithPaged(isLoading: false));
} }
} }
@@ -36,7 +40,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
/// Updates document filter and automatically reloads documents. Always resets page to 1. /// Updates document filter and automatically reloads documents. Always resets page to 1.
/// Use [loadMore] to load more data. /// Use [loadMore] to load more data.
Future<void> updateFilter({ Future<void> updateFilter({
final DocumentFilter filter = DocumentFilter.initial, final DocumentFilter filter = const DocumentFilter(),
}) async { }) async {
try { try {
emit(state.copyWithPaged(isLoading: true)); emit(state.copyWithPaged(isLoading: true));
@@ -48,6 +52,8 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
hasLoaded: true, hasLoaded: true,
)); ));
} finally { } finally {
account.settings.currentDocumentFilter = filter;
account.save();
emit(state.copyWithPaged(isLoading: false)); emit(state.copyWithPaged(isLoading: false));
} }
} }
@@ -65,13 +71,15 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
sortField: state.filter.sortField, sortField: state.filter.sortField,
sortOrder: state.filter.sortOrder, sortOrder: state.filter.sortOrder,
); );
account.settings.currentDocumentFilter = filter;
account.save();
return updateFilter(filter: filter); return updateFilter(filter: filter);
} }
Future<void> reload() async { Future<void> reload() async {
emit(state.copyWithPaged(isLoading: true)); emit(state.copyWithPaged(isLoading: true));
try {
final filter = state.filter.copyWith(page: 1); final filter = state.filter.copyWith(page: 1);
try {
final result = await api.findAll(filter); final result = await api.findAll(filter);
if (!isClosed) { if (!isClosed) {
emit(state.copyWithPaged( emit(state.copyWithPaged(
@@ -82,6 +90,8 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
)); ));
} }
} finally { } finally {
account.settings.currentDocumentFilter = filter;
account.save();
if (!isClosed) { if (!isClosed) {
emit(state.copyWithPaged(isLoading: false)); emit(state.copyWithPaged(isLoading: false));
} }
@@ -106,7 +116,6 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
try { try {
await api.delete(document); await api.delete(document);
notifier.notifyDeleted(document); notifier.notifyDeleted(document);
// remove(document); // Removing deleted now works with the change notifier.
} finally { } finally {
emit(state.copyWithPaged(isLoading: false)); emit(state.copyWithPaged(isLoading: false));
} }

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
/// ///

View File

@@ -12,7 +12,7 @@ class SavedViewDetailsState extends DocumentPagingState {
const SavedViewDetailsState({ const SavedViewDetailsState({
this.viewType = ViewType.list, this.viewType = ViewType.list,
super.filter, super.filter = const DocumentFilter(),
super.hasLoaded, super.hasLoaded,
super.isLoading, super.isLoading,
super.value, super.value,

View File

@@ -29,7 +29,4 @@ class GlobalSettings with HiveObjectMixin {
this.showOnboarding = true, this.showOnboarding = true,
this.currentLoggedInUser, this.currentLoggedInUser,
}); });
static GlobalSettings get boxedValue =>
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
} }

View File

@@ -1,4 +1,5 @@
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
part 'user_settings.g.dart'; part 'user_settings.g.dart';
@@ -8,7 +9,11 @@ class UserSettings with HiveObjectMixin {
@HiveField(0) @HiveField(0)
bool isBiometricAuthenticationEnabled; bool isBiometricAuthenticationEnabled;
@HiveField(1)
DocumentFilter currentDocumentFilter;
UserSettings({ UserSettings({
this.isBiometricAuthenticationEnabled = false, this.isBiometricAuthenticationEnabled = false,
required this.currentDocumentFilter,
}); });
} }

View File

@@ -1,7 +1,9 @@
import 'dart:ui';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.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/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -14,68 +16,58 @@ import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_d
import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart'; import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class ManageAccountsPage extends StatelessWidget { class ManageAccountsPage extends StatelessWidget {
const ManageAccountsPage({super.key}); const ManageAccountsPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Dialog.fullscreen( return GlobalSettingsBuilder(
child: Scaffold(
appBar: AppBar(
leading: const CloseButton(),
title: const Text("Accounts"), //TODO: INTL
),
body: GlobalSettingsBuilder(
builder: (context, globalSettings) { builder: (context, globalSettings) {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(), valueListenable:
Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
builder: (context, box, _) { builder: (context, box, _) {
final userIds = box.keys.toList().cast<String>(); final userIds = box.keys.toList().cast<String>();
final otherAccounts = userIds final otherAccounts = userIds
.whereNot((element) => element == globalSettings.currentLoggedInUser) .whereNot(
(element) => element == globalSettings.currentLoggedInUser)
.toList(); .toList();
return CustomScrollView( return SimpleDialog(
slivers: [ insetPadding: EdgeInsets.all(24),
SliverToBoxAdapter( contentPadding: EdgeInsets.all(8),
child: Column( title: Stack(
crossAxisAlignment: CrossAxisAlignment.start, alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: CloseButton(),
),
Center(child: Text("Accounts")),
],
), //TODO: INTL
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
children: [ children: [
Text(
"Your account", //TODO: INTL
style: Theme.of(context).textTheme.labelLarge,
).padded(16),
_buildAccountTile( _buildAccountTile(
context, context,
globalSettings.currentLoggedInUser!, globalSettings.currentLoggedInUser!,
box.get(globalSettings.currentLoggedInUser!)!, box.get(globalSettings.currentLoggedInUser!)!,
globalSettings, globalSettings),
), // if (otherAccounts.isNotEmpty) Text("Other accounts"),
if (otherAccounts.isNotEmpty) const Divider(), Column(
], children: [
), for (int index = 0; index < otherAccounts.length; index++)
), _buildAccountTile(
if (otherAccounts.isNotEmpty)
SliverToBoxAdapter(
child: Text(
"Other accounts", //TODO: INTL
style: Theme.of(context).textTheme.labelLarge,
).padded(16),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildAccountTile(
context, context,
otherAccounts[index], otherAccounts[index],
box.get(otherAccounts[index])!, box.get(otherAccounts[index])!,
globalSettings, globalSettings,
), ),
childCount: otherAccounts.length, ],
), ),
),
SliverToBoxAdapter(
child: Column(
children: [
const Divider(), const Divider(),
ListTile( ListTile(
title: const Text("Add account"), title: const Text("Add account"),
@@ -84,21 +76,11 @@ class ManageAccountsPage extends StatelessWidget {
_onAddAccount(context); _onAddAccount(context);
}, },
), ),
// FilledButton.tonalIcon(
// icon: Icon(Icons.person_add),
// label: Text("Add account"),
// onPressed: () {},
// ),
],
),
),
], ],
); );
}, },
); );
}, },
),
),
); );
} }
@@ -108,14 +90,22 @@ class ManageAccountsPage extends StatelessWidget {
UserAccount account, UserAccount account,
GlobalSettings settings, GlobalSettings settings,
) { ) {
final isLoggedIn = userId == settings.currentLoggedInUser;
final theme = Theme.of(context); final theme = Theme.of(context);
return ListTile( final child = SizedBox(
width: double.maxFinite,
child: ListTile(
title: Text(account.username), title: Text(account.username),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (account.fullName != null) Text(account.fullName!), if (account.fullName != null) Text(account.fullName!),
Text(account.serverUrl), Text(
account.serverUrl.replaceFirst(RegExp(r'https://?'), ''),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
], ],
), ),
isThreeLine: true, isThreeLine: true,
@@ -127,19 +117,31 @@ class ManageAccountsPage extends StatelessWidget {
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
if (settings.currentLoggedInUser != userId) if (!isLoggedIn)
const PopupMenuItem( const PopupMenuItem(
child: ListTile( child: ListTile(
title: Text("Switch"), //TODO: INTL title: Text("Switch"), //TODO: INTL
leading: Icon(Icons.switch_account_outlined), leading: Icon(Icons.switch_account_rounded),
), ),
value: 0, value: 0,
), ),
if (!isLoggedIn)
const PopupMenuItem( const PopupMenuItem(
child: ListTile( child: ListTile(
title: Text("Remove"), // TODO: INTL title: Text("Remove"), // TODO: INTL
leading: Icon( leading: Icon(
Icons.remove_circle_outline, Icons.person_remove,
color: Colors.red,
),
),
value: 1,
)
else
const PopupMenuItem(
child: ListTile(
title: Text("Logout"), // TODO: INTL
leading: Icon(
Icons.person_remove,
color: Colors.red, color: Colors.red,
), ),
), ),
@@ -170,7 +172,14 @@ class ManageAccountsPage extends StatelessWidget {
} }
}, },
), ),
),
); );
if (isLoggedIn) {
return Card(
child: child,
);
}
return child;
} }
Future<void> _onAddAccount(BuildContext context) { Future<void> _onAddAccount(BuildContext context) {
@@ -179,7 +188,8 @@ class ManageAccountsPage extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => LoginPage( builder: (context) => LoginPage(
titleString: "Add account", //TODO: INTL titleString: "Add account", //TODO: INTL
onSubmit: (context, username, password, serverUrl, clientCertificate) async { onSubmit: (context, username, password, serverUrl,
clientCertificate) async {
final userId = await context.read<AuthenticationCubit>().addAccount( final userId = await context.read<AuthenticationCubit>().addAccount(
credentials: LoginFormCredentials( credentials: LoginFormCredentials(
username: username, username: username,
@@ -192,8 +202,8 @@ class ManageAccountsPage extends StatelessWidget {
); );
final shoudSwitch = await showDialog( final shoudSwitch = await showDialog(
context: context, context: context,
builder: (context) => builder: (context) => SwitchAccountDialog(
SwitchAccountDialog(username: username, serverUrl: serverUrl), username: username, serverUrl: serverUrl),
) ?? ) ??
false; false;
if (shoudSwitch) { if (shoudSwitch) {

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
class SwitchingAccountsPage extends StatelessWidget { class SwitchingAccountsPage extends StatelessWidget {
const SwitchingAccountsPage({super.key}); const SwitchingAccountsPage({super.key});
@@ -13,7 +11,14 @@ class SwitchingAccountsPage extends StatelessWidget {
}, },
child: Material( child: Material(
child: Center( child: Center(
child: Text("Switching accounts. Please wait..."), child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Text("Switching accounts. Please wait..."),
],
),
), ),
), ),
); );

View File

@@ -14,13 +14,13 @@ class BiometricAuthenticationSetting extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return UserSettingsBuilder( return UserAccountBuilder(
builder: (context, settings) { builder: (context, account) {
if (settings == null) { if (account == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return SwitchListTile( return SwitchListTile(
value: settings.isBiometricAuthenticationEnabled, value: account.settings.isBiometricAuthenticationEnabled,
title: Text(S.of(context)!.biometricAuthentication), title: Text(S.of(context)!.biometricAuthentication),
subtitle: Text(S.of(context)!.authenticateOnAppStart), subtitle: Text(S.of(context)!.authenticateOnAppStart),
onChanged: (val) async { onChanged: (val) async {
@@ -33,8 +33,8 @@ class BiometricAuthenticationSetting extends StatelessWidget {
.read<LocalAuthenticationService>() .read<LocalAuthenticationService>()
.authenticateLocalUser(localizedReason); .authenticateLocalUser(localizedReason);
if (isAuthenticated) { if (isAuthenticated) {
settings.isBiometricAuthenticationEnabled = val; account.settings.isBiometricAuthenticationEnabled = val;
settings.save(); account.save();
} }
}, },
); );

View File

@@ -1,30 +1,33 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.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/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/model/user_settings.dart'; import 'package:paperless_mobile/features/settings/model/user_settings.dart';
class UserSettingsBuilder extends StatelessWidget { class UserAccountBuilder extends StatelessWidget {
final Widget Function( final Widget Function(
BuildContext context, BuildContext context,
UserSettings? settings, UserAccount? settings,
) builder; ) builder;
const UserSettingsBuilder({ const UserAccountBuilder({
super.key, super.key,
required this.builder, required this.builder,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder<Box<UserSettings>>( return ValueListenableBuilder<Box<UserAccount>>(
valueListenable: Hive.box<UserSettings>(HiveBoxes.userSettings).listenable(), valueListenable:
builder: (context, value, _) { Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
final currentUser = builder: (context, accountBox, _) {
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser;
if (currentUser != null) { if (currentUser != null) {
final settings = value.get(currentUser); final account = accountBox.get(currentUser);
return builder(context, settings); return builder(context, account);
} else { } else {
return builder(context, null); return builder(context, null);
} }

View File

@@ -24,7 +24,7 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
this.notifier, this.notifier,
this._labelRepository, { this._labelRepository, {
required this.documentId, required this.documentId,
}) : super(const SimilarDocumentsState()) { }) : super(SimilarDocumentsState(filter: DocumentFilter())) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,

View File

@@ -7,7 +7,7 @@ class SimilarDocumentsState extends DocumentPagingState {
final Map<int, StoragePath> storagePaths; final Map<int, StoragePath> storagePaths;
const SimilarDocumentsState({ const SimilarDocumentsState({
super.filter, required super.filter,
super.hasLoaded, super.hasLoaded,
super.isLoading, super.isLoading,
super.value, super.value,

View File

@@ -54,7 +54,8 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
String get defaultPreferredLocaleSubtag { String get defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first; String preferredLocale = Platform.localeName.split("_").first;
if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) { if (!S.supportedLocales
.any((locale) => locale.languageCode == preferredLocale)) {
preferredLocale = 'en'; preferredLocale = 'en';
} }
return preferredLocale; return preferredLocale;
@@ -63,16 +64,18 @@ String get defaultPreferredLocaleSubtag {
Future<void> _initHive() async { Future<void> _initHive() async {
await Hive.initFlutter(); await Hive.initFlutter();
//TODO: REMOVE! //TODO: REMOVE!
// await getApplicationDocumentsDirectory().then((value) => value.delete(recursive: true)); // await getApplicationDocumentsDirectory()
// .then((value) => value.delete(recursive: true));
registerHiveAdapters(); registerHiveAdapters();
await Hive.openBox<UserAccount>(HiveBoxes.userAccount); await Hive.openBox<UserAccount>(HiveBoxes.userAccount);
await Hive.openBox<UserSettings>(HiveBoxes.userSettings); final globalSettingsBox =
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings); await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.hasValue) { if (!globalSettingsBox.hasValue) {
await globalSettingsBox await globalSettingsBox.setValue(
.setValue(GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag)); GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag),
);
} }
} }
@@ -152,7 +155,8 @@ void main() async {
//Update language header in interceptor on language change. //Update language header in interceptor on language change.
globalSettingsBox.listenable().addListener(() { globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag; languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag;
}); });
runApp( runApp(
@@ -176,7 +180,8 @@ void main() async {
Provider<ConnectivityStatusService>.value( Provider<ConnectivityStatusService>.value(
value: connectivityStatusService, value: connectivityStatusService,
), ),
Provider<LocalNotificationService>.value(value: localNotificationService), Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()), Provider.value(value: DocumentChangedNotifier()),
], ],
child: MultiRepositoryProvider( child: MultiRepositoryProvider(
@@ -206,7 +211,8 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState(); State<PaperlessMobileEntrypoint> createState() =>
_PaperlessMobileEntrypointState();
} }
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> { class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
@@ -241,7 +247,8 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], ],
routes: { routes: {
DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(), DocumentDetailsRoute.routeName: (context) =>
const DocumentDetailsRoute(),
}, },
home: const AuthenticationWrapper(), home: const AuthenticationWrapper(),
); );
@@ -276,9 +283,11 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
} }
initializeDateFormatting(); initializeDateFormatting();
// For sharing files coming from outside the app while the app is still opened // For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream().listen(ShareIntentQueue.instance.addAll); ReceiveSharingIntent.getMediaStream()
.listen(ShareIntentQueue.instance.addAll);
// For sharing files coming from outside the app while the app is closed // For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then(ShareIntentQueue.instance.addAll); ReceiveSharingIntent.getInitialMedia()
.then(ShareIntentQueue.instance.addAll);
} }
Future<void> _setOptimalDisplayMode() async { Future<void> _setOptimalDisplayMode() async {
@@ -290,7 +299,8 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
.toList() .toList()
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate)); ..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active; final DisplayMode mostOptimalMode =
sameResolution.isNotEmpty ? sameResolution.first : active;
debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}'); debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
await FlutterDisplayMode.setPreferredMode(mostOptimalMode); await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
@@ -341,12 +351,14 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
) async { ) async {
try { try {
await context.read<AuthenticationCubit>().login( await context.read<AuthenticationCubit>().login(
credentials: LoginFormCredentials(username: username, password: password), credentials:
LoginFormCredentials(username: username, password: password),
serverUrl: serverUrl, serverUrl: serverUrl,
clientCertificate: clientCertificate, clientCertificate: clientCertificate,
); );
// Show onboarding after first login! // Show onboarding after first login!
final globalSettings = GlobalSettings.boxedValue; final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.showOnboarding) { if (globalSettings.showOnboarding) {
Navigator.push( Navigator.push(
context, context,

View File

@@ -0,0 +1,4 @@
class PaperlessApiHiveTypeIds {
PaperlessApiHiveTypeIds._();
static const int documentFilter = 1000;
}

View File

@@ -1,6 +1,8 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/config/hive/hive_type_ids.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/converters/tags_query_json_converter.dart'; import 'package:paperless_api/src/converters/tags_query_json_converter.dart';
@@ -9,6 +11,7 @@ part 'document_filter.g.dart';
@TagsQueryJsonConverter() @TagsQueryJsonConverter()
@DateRangeQueryJsonConverter() @DateRangeQueryJsonConverter()
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
@HiveType(typeId: PaperlessApiHiveTypeIds.documentFilter)
class DocumentFilter extends Equatable { class DocumentFilter extends Equatable {
static const DocumentFilter initial = DocumentFilter(); static const DocumentFilter initial = DocumentFilter();
@@ -19,21 +22,35 @@ class DocumentFilter extends Equatable {
page: 1, page: 1,
); );
@HiveField(0)
final int pageSize; final int pageSize;
@HiveField(1)
final int page; final int page;
@HiveField(2)
final IdQueryParameter documentType; final IdQueryParameter documentType;
@HiveField(3)
final IdQueryParameter correspondent; final IdQueryParameter correspondent;
@HiveField(4)
final IdQueryParameter storagePath; final IdQueryParameter storagePath;
@HiveField(5)
final IdQueryParameter asnQuery; final IdQueryParameter asnQuery;
@HiveField(6)
final TagsQuery tags; final TagsQuery tags;
@HiveField(7)
final SortField? sortField; final SortField? sortField;
@HiveField(8)
final SortOrder sortOrder; final SortOrder sortOrder;
@HiveField(9)
final DateRangeQuery created; final DateRangeQuery created;
@HiveField(10)
final DateRangeQuery added; final DateRangeQuery added;
@HiveField(11)
final DateRangeQuery modified; final DateRangeQuery modified;
@HiveField(12)
final TextQuery query; final TextQuery query;
/// Query documents similar to the document with this id. /// Query documents similar to the document with this id.
@HiveField(13)
final int? moreLike; final int? moreLike;
const DocumentFilter({ const DocumentFilter({

View File

@@ -152,10 +152,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override @override
Future<int> findNextAsn() async { Future<int> findNextAsn() async {
const DocumentFilter asnQueryFilter = DocumentFilter( final DocumentFilter asnQueryFilter = DocumentFilter(
sortField: SortField.archiveSerialNumber, sortField: SortField.archiveSerialNumber,
sortOrder: SortOrder.descending, sortOrder: SortOrder.descending,
asnQuery: IdQueryParameter.anyAssigned(), asnQuery: const IdQueryParameter.anyAssigned(),
page: 1, page: 1,
pageSize: 1, pageSize: 1,
); );

View File

@@ -21,6 +21,7 @@ dependencies:
collection: ^1.17.0 collection: ^1.17.0
jiffy: ^5.0.0 jiffy: ^5.0.0
freezed_annotation: ^2.2.0 freezed_annotation: ^2.2.0
hive: ^2.2.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -29,6 +30,7 @@ dev_dependencies:
json_serializable: ^6.5.4 json_serializable: ^6.5.4
build_runner: ^2.3.2 build_runner: ^2.3.2
freezed: ^2.3.2 freezed: ^2.3.2
hive_generator: ^2.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View File

@@ -210,16 +210,16 @@ void main() {
test('Values are correctly parsed if unset.', () { test('Values are correctly parsed if unset.', () {
expect( expect(
SavedView.fromDocumentFilter( SavedView.fromDocumentFilter(
const DocumentFilter( DocumentFilter(
correspondent: IdQueryParameter.unset(), correspondent: const IdQueryParameter.unset(),
documentType: IdQueryParameter.unset(), documentType: const IdQueryParameter.unset(),
storagePath: IdQueryParameter.unset(), storagePath: const IdQueryParameter.unset(),
tags: IdsTagsQuery(), tags: const IdsTagsQuery(),
sortField: SortField.created, sortField: SortField.created,
sortOrder: SortOrder.descending, sortOrder: SortOrder.descending,
added: UnsetDateRangeQuery(), added: const UnsetDateRangeQuery(),
created: UnsetDateRangeQuery(), created: const UnsetDateRangeQuery(),
query: TextQuery(), query: const TextQuery(),
), ),
name: "test_name", name: "test_name",
showInSidebar: false, showInSidebar: false,
@@ -241,11 +241,11 @@ void main() {
test('Values are correctly parsed if not assigned.', () { test('Values are correctly parsed if not assigned.', () {
expect( expect(
SavedView.fromDocumentFilter( SavedView.fromDocumentFilter(
const DocumentFilter( DocumentFilter(
correspondent: IdQueryParameter.notAssigned(), correspondent: const IdQueryParameter.notAssigned(),
documentType: IdQueryParameter.notAssigned(), documentType: const IdQueryParameter.notAssigned(),
storagePath: IdQueryParameter.notAssigned(), storagePath: const IdQueryParameter.notAssigned(),
tags: OnlyNotAssignedTagsQuery(), tags: const OnlyNotAssignedTagsQuery(),
sortField: SortField.created, sortField: SortField.created,
sortOrder: SortOrder.ascending, sortOrder: SortOrder.ascending,
), ),