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 {
release {
signingConfig signingConfigs.release
signingConfig signingConfigs.debug
}
}

View File

@@ -1,5 +1,6 @@
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/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
@@ -38,6 +39,7 @@ void registerHiveAdapters() {
Hive.registerAdapter(UserSettingsAdapter());
Hive.registerAdapter(UserCredentialsAdapter());
Hive.registerAdapter(UserAccountAdapter());
Hive.registerAdapter(DocumentFilterAdapter());
}
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_mobile/core/notifier/document_changed_notifier.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:json_annotation/json_annotation.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';
part 'document_search_state.dart';
part 'document_search_cubit.g.dart';
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
@@ -20,7 +21,6 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
final LabelRepository _labelRepository;
@override
final DocumentChangedNotifier notifier;
DocumentSearchCubit(this.api, this.notifier, this._labelRepository)
: super(const DocumentSearchState()) {
_labelRepository.addListener(
@@ -119,4 +119,8 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
Map<String, dynamic>? toJson(DocumentSearchState state) {
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.suggestions = const [],
this.viewType = ViewType.detailed,
super.filter,
super.filter = const DocumentFilter(),
super.hasLoaded,
super.isLoading,
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/config/hive/hive_config.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/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
@@ -45,10 +46,14 @@ class SliverSearchBar extends StatelessWidget {
icon: GlobalSettingsBuilder(
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
valueListenable:
Hive.box<UserAccount>(HiveBoxes.userAccount)
.listenable(),
builder: (context, box, _) {
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_mobile/core/notifier/document_changed_notifier.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/paged_documents_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -23,8 +24,15 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
@override
final DocumentChangedNotifier notifier;
DocumentsCubit(this.api, this.notifier, this._labelRepository)
: super(const DocumentsState()) {
@override
final UserAccount account;
DocumentsCubit(
this.api,
this.notifier,
this._labelRepository,
this.account,
) : super(DocumentsState(filter: account.settings.currentDocumentFilter)) {
notifier.addListener(
this,
onUpdated: (document) {

View File

@@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.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/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/repository/label_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/view/pages/labels_page.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/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
@@ -230,7 +233,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
return icon;
},
)),
),
),
];
final routes = <Widget>[
MultiBlocProvider(
@@ -241,6 +245,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
context.read(),
context.read(),
context.read(),
Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!,
)..reload(),
),
BlocProvider(
@@ -275,7 +280,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
listeners: [
BlocListener<ConnectivityCubit, ConnectivityState>(
//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) {
_initializeData(context);
},
@@ -284,7 +290,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
listener: (context, state) {
if (state.task != null) {
// 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: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(),
destinations: destinations
.map((e) => e.toNavigationRailDestination())
.toList(),
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
),
@@ -315,7 +325,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
elevation: 4.0,
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
destinations: destinations.map((e) => e.toNavigationDestination()).toList(),
destinations:
destinations.map((e) => e.toNavigationDestination()).toList(),
),
body: routes[_currentIndex],
);

View File

@@ -24,7 +24,7 @@ class VerifyIdentityPage extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.background,
title: Text(S.of(context)!.verifyYourIdentity),
),
body: UserSettingsBuilder(
body: UserAccountBuilder(
builder: (context, settings) {
if (settings == null) {
return const SizedBox.shrink();
@@ -32,7 +32,9 @@ class VerifyIdentityPage extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
Text(S
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16),
const Icon(
Icons.fingerprint,
@@ -54,7 +56,9 @@ class VerifyIdentityPage extends StatelessWidget {
),
),
ElevatedButton(
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(),
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_state.dart';
class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin {
class InboxCubit extends HydratedCubit<InboxState>
with DocumentPagingBlocMixin {
final LabelRepository _labelRepository;
final PaperlessDocumentsApi _documentsApi;
@@ -31,12 +32,17 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
this._statsApi,
this._labelRepository,
this.notifier,
) : super(InboxState(labels: _labelRepository.state)) {
) : super(InboxState(
labels: _labelRepository.state,
)) {
notifier.addListener(
this,
onDeleted: remove,
onUpdated: (document) {
if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) {
if (document.tags
.toSet()
.intersection(state.inboxTags.toSet())
.isEmpty) {
remove(document);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
} else {
@@ -135,7 +141,8 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
/// from the inbox.
///
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 updatedDocument = await api.update(
@@ -189,8 +196,8 @@ class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin
Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) {
final int asn = await _documentsApi.findNextAsn();
final updatedDocument =
await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn));
final updatedDocument = await _documentsApi
.update(document.copyWith(archiveSerialNumber: () => asn));
replace(updatedDocument);
}

View File

@@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
class LabelItem<T extends Label> extends StatelessWidget {
@@ -42,6 +46,10 @@ class LabelItem<T extends Label> extends StatelessWidget {
onPressed: (label.documentCount ?? 0) == 0
? null
: () {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
final filter = filterBuilder(label);
Navigator.push(
context,
@@ -52,6 +60,8 @@ class LabelItem<T extends Label> extends StatelessWidget {
context.read(),
context.read(),
context.read(),
Hive.box<UserAccount>(HiveBoxes.userAccount)
.get(currentUser)!,
),
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_mobile/core/notifier/document_changed_notifier.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/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -20,12 +21,16 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
final LabelRepository _labelRepository;
@override
// TODO: implement account
final UserAccount account;
LinkedDocumentsCubit(
DocumentFilter filter,
this.api,
this.notifier,
this._labelRepository,
) : super(const LinkedDocumentsState()) {
this.account,
) : super(LinkedDocumentsState(filter: filter)) {
updateFilter(filter: filter);
_labelRepository.addListener(
this,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
class SwitchingAccountsPage extends StatelessWidget {
const SwitchingAccountsPage({super.key});
@@ -13,7 +11,14 @@ class SwitchingAccountsPage extends StatelessWidget {
},
child: Material(
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
Widget build(BuildContext context) {
return UserSettingsBuilder(
builder: (context, settings) {
if (settings == null) {
return UserAccountBuilder(
builder: (context, account) {
if (account == null) {
return const SizedBox.shrink();
}
return SwitchListTile(
value: settings.isBiometricAuthenticationEnabled,
value: account.settings.isBiometricAuthenticationEnabled,
title: Text(S.of(context)!.biometricAuthentication),
subtitle: Text(S.of(context)!.authenticateOnAppStart),
onChanged: (val) async {
@@ -33,8 +33,8 @@ class BiometricAuthenticationSetting extends StatelessWidget {
.read<LocalAuthenticationService>()
.authenticateLocalUser(localizedReason);
if (isAuthenticated) {
settings.isBiometricAuthenticationEnabled = val;
settings.save();
account.settings.isBiometricAuthenticationEnabled = val;
account.save();
}
},
);

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,8 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
String get defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first;
if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) {
if (!S.supportedLocales
.any((locale) => locale.languageCode == preferredLocale)) {
preferredLocale = 'en';
}
return preferredLocale;
@@ -63,16 +64,18 @@ String get defaultPreferredLocaleSubtag {
Future<void> _initHive() async {
await Hive.initFlutter();
//TODO: REMOVE!
// await getApplicationDocumentsDirectory().then((value) => value.delete(recursive: true));
// await getApplicationDocumentsDirectory()
// .then((value) => value.delete(recursive: true));
registerHiveAdapters();
await Hive.openBox<UserAccount>(HiveBoxes.userAccount);
await Hive.openBox<UserSettings>(HiveBoxes.userSettings);
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
final globalSettingsBox =
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.hasValue) {
await globalSettingsBox
.setValue(GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag));
await globalSettingsBox.setValue(
GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag),
);
}
}
@@ -152,7 +155,8 @@ void main() async {
//Update language header in interceptor on language change.
globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag;
languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag;
});
runApp(
@@ -176,7 +180,8 @@ void main() async {
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService,
),
Provider<LocalNotificationService>.value(value: localNotificationService),
Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()),
],
child: MultiRepositoryProvider(
@@ -206,7 +211,8 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
}) : super(key: key);
@override
State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState();
State<PaperlessMobileEntrypoint> createState() =>
_PaperlessMobileEntrypointState();
}
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
@@ -241,7 +247,8 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate,
],
routes: {
DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(),
DocumentDetailsRoute.routeName: (context) =>
const DocumentDetailsRoute(),
},
home: const AuthenticationWrapper(),
);
@@ -276,9 +283,11 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
}
initializeDateFormatting();
// 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
ReceiveSharingIntent.getInitialMedia().then(ShareIntentQueue.instance.addAll);
ReceiveSharingIntent.getInitialMedia()
.then(ShareIntentQueue.instance.addAll);
}
Future<void> _setOptimalDisplayMode() async {
@@ -290,7 +299,8 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
.toList()
..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}');
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
@@ -341,12 +351,14 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
) async {
try {
await context.read<AuthenticationCubit>().login(
credentials: LoginFormCredentials(username: username, password: password),
credentials:
LoginFormCredentials(username: username, password: password),
serverUrl: serverUrl,
clientCertificate: clientCertificate,
);
// Show onboarding after first login!
final globalSettings = GlobalSettings.boxedValue;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.showOnboarding) {
Navigator.push(
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:equatable/equatable.dart';
import 'package:hive/hive.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/src/converters/tags_query_json_converter.dart';
@@ -9,6 +11,7 @@ part 'document_filter.g.dart';
@TagsQueryJsonConverter()
@DateRangeQueryJsonConverter()
@JsonSerializable(explicitToJson: true)
@HiveType(typeId: PaperlessApiHiveTypeIds.documentFilter)
class DocumentFilter extends Equatable {
static const DocumentFilter initial = DocumentFilter();
@@ -19,21 +22,35 @@ class DocumentFilter extends Equatable {
page: 1,
);
@HiveField(0)
final int pageSize;
@HiveField(1)
final int page;
@HiveField(2)
final IdQueryParameter documentType;
@HiveField(3)
final IdQueryParameter correspondent;
@HiveField(4)
final IdQueryParameter storagePath;
@HiveField(5)
final IdQueryParameter asnQuery;
@HiveField(6)
final TagsQuery tags;
@HiveField(7)
final SortField? sortField;
@HiveField(8)
final SortOrder sortOrder;
@HiveField(9)
final DateRangeQuery created;
@HiveField(10)
final DateRangeQuery added;
@HiveField(11)
final DateRangeQuery modified;
@HiveField(12)
final TextQuery query;
/// Query documents similar to the document with this id.
@HiveField(13)
final int? moreLike;
const DocumentFilter({

View File

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

View File

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

View File

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