feat: Implement switching between accounts (multi user support), still WIP

This commit is contained in:
Anton Stubenbord
2023-04-21 01:32:43 +02:00
parent 1334f546ee
commit 95dd0a2405
50 changed files with 1055 additions and 721 deletions

View File

@@ -2,16 +2,19 @@ import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/custpm_adapters/theme_mode_adapter.dart'; import 'package:paperless_mobile/core/config/hive/custpm_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/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/user_settings.dart';
class HiveBoxes { class HiveBoxes {
HiveBoxes._(); HiveBoxes._();
static const globalSettings = 'globalSettings'; static const globalSettings = 'globalSettings';
static const userSettings = 'userSettings'; static const userSettings = 'userSettings';
static const authentication = 'authentication'; static const authentication = 'authentication';
static const vault = 'vault'; static const userCredentials = 'userCredentials';
static const userAccount = 'userAccount';
} }
class HiveTypeIds { class HiveTypeIds {
@@ -22,18 +25,26 @@ class HiveTypeIds {
static const colorSchemeOption = 3; static const colorSchemeOption = 3;
static const authentication = 4; static const authentication = 4;
static const clientCertificate = 5; static const clientCertificate = 5;
} static const userCredentials = 6;
static const userAccount = 7;
class HiveBoxSingleValueKey {
HiveBoxSingleValueKey._();
static const value = 'value';
} }
void registerHiveAdapters() { void registerHiveAdapters() {
Hive.registerAdapter(ColorSchemeOptionAdapter()); Hive.registerAdapter(ColorSchemeOptionAdapter());
Hive.registerAdapter(ThemeModeAdapter()); Hive.registerAdapter(ThemeModeAdapter());
Hive.registerAdapter(GlobalAppSettingsAdapter()); Hive.registerAdapter(GlobalSettingsAdapter());
Hive.registerAdapter(UserAppSettingsAdapter());
Hive.registerAdapter(AuthenticationInformationAdapter()); Hive.registerAdapter(AuthenticationInformationAdapter());
Hive.registerAdapter(ClientCertificateAdapter()); Hive.registerAdapter(ClientCertificateAdapter());
Hive.registerAdapter(UserSettingsAdapter());
Hive.registerAdapter(UserCredentialsAdapter());
Hive.registerAdapter(UserAccountAdapter());
}
extension HiveSingleValueBox<T> on Box<T> {
static const _valueKey = 'SINGLE_VALUE';
bool get hasValue => containsKey(_valueKey);
T? getValue() => get(_valueKey);
Future<void> setValue(T value) => put(_valueKey, value);
} }

View File

@@ -38,8 +38,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Tag> createTag(Tag object) async { Future<Tag> createTag(Tag object) async {
final created = await _api.saveTag(object); final created = await _api.saveTag(object);
final updatedState = {...state.tags} final updatedState = {...state.tags}..putIfAbsent(created.id!, () => created);
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(tags: updatedState)); emit(state.copyWith(tags: updatedState));
return created; return created;
} }
@@ -63,8 +62,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async { Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids); final tags = await _api.getTags(ids);
final updatedState = {...state.tags} final updatedState = {...state.tags}..addEntries(tags.map((e) => MapEntry(e.id!, e)));
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(tags: updatedState)); emit(state.copyWith(tags: updatedState));
return tags; return tags;
} }
@@ -78,16 +76,14 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Correspondent> createCorrespondent(Correspondent correspondent) async { Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent); final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.correspondents} final updatedState = {...state.correspondents}..putIfAbsent(created.id!, () => created);
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(correspondents: updatedState)); emit(state.copyWith(correspondents: updatedState));
return created; return created;
} }
Future<int> deleteCorrespondent(Correspondent correspondent) async { Future<int> deleteCorrespondent(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent); await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.correspondents} final updatedState = {...state.correspondents}..removeWhere((k, v) => k == correspondent.id);
..removeWhere((k, v) => k == correspondent.id);
emit(state.copyWith(correspondents: updatedState)); emit(state.copyWith(correspondents: updatedState));
return correspondent.id!; return correspondent.id!;
@@ -104,8 +100,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return null; return null;
} }
Future<Iterable<Correspondent>> findAllCorrespondents( Future<Iterable<Correspondent>> findAllCorrespondents([Iterable<int>? ids]) async {
[Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids); final correspondents = await _api.getCorrespondents(ids);
final updatedState = {...state.correspondents} final updatedState = {...state.correspondents}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e))); ..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
@@ -116,8 +111,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async { Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent); final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.correspondents} final updatedState = {...state.correspondents}..update(updated.id!, (_) => updated);
..update(updated.id!, (_) => updated);
emit(state.copyWith(correspondents: updatedState)); emit(state.copyWith(correspondents: updatedState));
return updated; return updated;
@@ -125,16 +119,14 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<DocumentType> createDocumentType(DocumentType documentType) async { Future<DocumentType> createDocumentType(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType); final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.documentTypes} final updatedState = {...state.documentTypes}..putIfAbsent(created.id!, () => created);
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(documentTypes: updatedState)); emit(state.copyWith(documentTypes: updatedState));
return created; return created;
} }
Future<int> deleteDocumentType(DocumentType documentType) async { Future<int> deleteDocumentType(DocumentType documentType) async {
await _api.deleteDocumentType(documentType); await _api.deleteDocumentType(documentType);
final updatedState = {...state.documentTypes} final updatedState = {...state.documentTypes}..removeWhere((k, v) => k == documentType.id);
..removeWhere((k, v) => k == documentType.id);
emit(state.copyWith(documentTypes: updatedState)); emit(state.copyWith(documentTypes: updatedState));
return documentType.id!; return documentType.id!;
} }
@@ -149,8 +141,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return null; return null;
} }
Future<Iterable<DocumentType>> findAllDocumentTypes( Future<Iterable<DocumentType>> findAllDocumentTypes([Iterable<int>? ids]) async {
[Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids); final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.documentTypes} final updatedState = {...state.documentTypes}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
@@ -160,24 +151,21 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<DocumentType> updateDocumentType(DocumentType documentType) async { Future<DocumentType> updateDocumentType(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType); final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.documentTypes} final updatedState = {...state.documentTypes}..update(updated.id!, (_) => updated);
..update(updated.id!, (_) => updated);
emit(state.copyWith(documentTypes: updatedState)); emit(state.copyWith(documentTypes: updatedState));
return updated; return updated;
} }
Future<StoragePath> createStoragePath(StoragePath storagePath) async { Future<StoragePath> createStoragePath(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath); final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.storagePaths} final updatedState = {...state.storagePaths}..putIfAbsent(created.id!, () => created);
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(storagePaths: updatedState)); emit(state.copyWith(storagePaths: updatedState));
return created; return created;
} }
Future<int> deleteStoragePath(StoragePath storagePath) async { Future<int> deleteStoragePath(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath); await _api.deleteStoragePath(storagePath);
final updatedState = {...state.storagePaths} final updatedState = {...state.storagePaths}..removeWhere((k, v) => k == storagePath.id);
..removeWhere((k, v) => k == storagePath.id);
emit(state.copyWith(storagePaths: updatedState)); emit(state.copyWith(storagePaths: updatedState));
return storagePath.id!; return storagePath.id!;
} }
@@ -192,8 +180,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return null; return null;
} }
Future<Iterable<StoragePath>> findAllStoragePaths( Future<Iterable<StoragePath>> findAllStoragePaths([Iterable<int>? ids]) async {
[Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids); final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.storagePaths} final updatedState = {...state.storagePaths}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
@@ -203,8 +190,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<StoragePath> updateStoragePath(StoragePath storagePath) async { Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath); final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.storagePaths} final updatedState = {...state.storagePaths}..update(updated.id!, (_) => updated);
..update(updated.id!, (_) => updated);
emit(state.copyWith(storagePaths: updatedState)); emit(state.copyWith(storagePaths: updatedState));
return updated; return updated;
} }
@@ -217,6 +203,12 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return super.close(); return super.close();
} }
@override
Future<void> clear() async {
await super.clear();
emit(const LabelRepositoryState());
}
@override @override
LabelRepositoryState? fromJson(Map<String, dynamic> json) { LabelRepositoryState? fromJson(Map<String, dynamic> json) {
return LabelRepositoryState.fromJson(json); return LabelRepositoryState.fromJson(json);

View File

@@ -27,8 +27,7 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
Future<SavedView> create(SavedView object) async { Future<SavedView> create(SavedView object) async {
final created = await _api.save(object); final created = await _api.save(object);
final updatedState = {...state.savedViews} final updatedState = {...state.savedViews}..putIfAbsent(created.id!, () => created);
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(savedViews: updatedState)); emit(state.copyWith(savedViews: updatedState));
return created; return created;
} }
@@ -43,8 +42,7 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
Future<SavedView?> find(int id) async { Future<SavedView?> find(int id) async {
final found = await _api.find(id); final found = await _api.find(id);
if (found != null) { if (found != null) {
final updatedState = {...state.savedViews} final updatedState = {...state.savedViews}..update(id, (_) => found, ifAbsent: () => found);
..update(id, (_) => found, ifAbsent: () => found);
emit(state.copyWith(savedViews: updatedState)); emit(state.copyWith(savedViews: updatedState));
} }
return found; return found;
@@ -68,6 +66,12 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
return super.close(); return super.close();
} }
@override
Future<void> clear() async {
await super.clear();
emit(const SavedViewRepositoryState());
}
@override @override
SavedViewRepositoryState? fromJson(Map<String, dynamic> json) { SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
return SavedViewRepositoryState.fromJson(json); return SavedViewRepositoryState.fromJson(json);

View File

@@ -32,8 +32,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
@override @override
Future<bool> isConnectedToInternet() async { Future<bool> isConnectedToInternet() async {
return _hasActiveInternetConnection( return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
await (Connectivity().checkConnectivity()));
} }
@override @override
@@ -72,8 +71,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
return ReachabilityStatus.unknown; return ReachabilityStatus.unknown;
} }
try { try {
SessionManager manager = SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()])
SessionManager([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate) ..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = const Duration(seconds: 5) ..client.options.connectTimeout = const Duration(seconds: 5)
..client.options.receiveTimeout = const Duration(seconds: 5); ..client.options.receiveTimeout = const Duration(seconds: 5);
@@ -84,8 +82,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
} }
return ReachabilityStatus.notReachable; return ReachabilityStatus.notReachable;
} on DioError catch (error) { } on DioError catch (error) {
if (error.type == DioErrorType.unknown && if (error.type == DioErrorType.unknown && error.error is ReachabilityStatus) {
error.error is ReachabilityStatus) {
return error.error as ReachabilityStatus; return error.error as ReachabilityStatus;
} }
} on TlsException catch (error) { } on TlsException catch (error) {

View File

@@ -9,11 +9,12 @@ import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart'; import 'package:paperless_mobile/core/model/document_processing_status.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/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/io.dart';
abstract class StatusService { abstract class StatusService {
Future<void> startListeningBeforeDocumentUpload(String httpUrl, Future<void> startListeningBeforeDocumentUpload(
AuthenticationInformation credentials, String documentFileName); String httpUrl, UserCredentials credentials, String documentFileName);
} }
class WebSocketStatusService implements StatusService { class WebSocketStatusService implements StatusService {
@@ -25,7 +26,7 @@ class WebSocketStatusService implements StatusService {
@override @override
Future<void> startListeningBeforeDocumentUpload( Future<void> startListeningBeforeDocumentUpload(
String httpUrl, String httpUrl,
AuthenticationInformation credentials, UserCredentials credentials,
String documentFileName, String documentFileName,
) async { ) async {
// socket = await WebSocket.connect( // socket = await WebSocket.connect(
@@ -57,7 +58,7 @@ class LongPollingStatusService implements StatusService {
@override @override
Future<void> startListeningBeforeDocumentUpload( Future<void> startListeningBeforeDocumentUpload(
String httpUrl, String httpUrl,
AuthenticationInformation credentials, UserCredentials credentials,
String documentFileName, String documentFileName,
) async { ) async {
// final today = DateTime.now(); // final today = DateTime.now();

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -42,8 +42,7 @@ class AppDrawer extends StatelessWidget {
leading: const Icon(Icons.bug_report_outlined), leading: const Icon(Icons.bug_report_outlined),
title: Text(S.of(context)!.reportABug), title: Text(S.of(context)!.reportABug),
onTap: () { onTap: () {
launchUrlString( launchUrlString('https://github.com/astubenbord/paperless-mobile/issues/new');
'https://github.com/astubenbord/paperless-mobile/issues/new');
}, },
), ),
ListTile( ListTile(
@@ -69,7 +68,10 @@ class AppDrawer extends StatelessWidget {
), ),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const SettingsPage(), builder: (_) => BlocProvider.value(
value: context.read<PaperlessServerInformationCubit>(),
child: const SettingsPage(),
),
), ),
), ),
), ),

View File

@@ -10,8 +10,7 @@ class ApplicationIntroSlideshow extends StatefulWidget {
const ApplicationIntroSlideshow({super.key}); const ApplicationIntroSlideshow({super.key});
@override @override
State<ApplicationIntroSlideshow> createState() => State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState();
_ApplicationIntroSlideshowState();
} }
//TODO: INTL ALL //TODO: INTL ALL
@@ -28,7 +27,9 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
showDoneButton: true, showDoneButton: true,
next: Text(S.of(context)!.next), next: Text(S.of(context)!.next),
done: Text(S.of(context)!.done), done: Text(S.of(context)!.done),
onDone: () => Navigator.pop(context), onDone: () {
Navigator.pop(context);
},
dotsDecorator: DotsDecorator( dotsDecorator: DotsDecorator(
color: Theme.of(context).colorScheme.onBackground, color: Theme.of(context).colorScheme.onBackground,
activeColor: Theme.of(context).colorScheme.primary, activeColor: Theme.of(context).colorScheme.primary,

View File

@@ -5,8 +5,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart'; import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -44,9 +43,8 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
width: 16, width: 16,
) )
: const Icon(Icons.download), : const Icon(Icons.download),
onPressed: widget.document != null && widget.enabled onPressed:
? () => _onDownload(widget.document!) widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null,
: null,
).paddedOnly(right: 4); ).paddedOnly(right: 4);
} }
@@ -70,7 +68,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
setState(() => _isDownloadPending = true); setState(() => _isDownloadPending = true);
await context.read<DocumentDetailsCubit>().downloadDocument( await context.read<DocumentDetailsCubit>().downloadDocument(
downloadOriginal: downloadOriginal, downloadOriginal: downloadOriginal,
locale: context.read<GlobalAppSettings>().preferredLocaleSubtag, locale: context.read<GlobalSettings>().preferredLocaleSubtag,
); );
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded); // showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {

View File

@@ -3,10 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.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/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.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' import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
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/settings/view/dialogs/account_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class SliverSearchBar extends StatelessWidget { class SliverSearchBar extends StatelessWidget {
@@ -37,8 +37,7 @@ class SliverSearchBar extends StatelessWidget {
onPressed: Scaffold.of(context).openDrawer, onPressed: Scaffold.of(context).openDrawer,
), ),
trailingIcon: IconButton( trailingIcon: IconButton(
icon: BlocBuilder<PaperlessServerInformationCubit, icon: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
PaperlessServerInformationState>(
builder: (context, state) { builder: (context, state) {
return CircleAvatar( return CircleAvatar(
child: Text(state.information?.userInitials ?? ''), child: Text(state.information?.userInitials ?? ''),
@@ -48,7 +47,10 @@ class SliverSearchBar extends StatelessWidget {
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) => const AccountSettingsDialog(), builder: (_) => BlocProvider.value(
value: context.read<PaperlessServerInformationCubit>(),
child: const ManageAccountsPage(),
),
); );
}, },
), ),

View File

@@ -46,9 +46,8 @@ class DocumentDetailedItem extends DocumentItem {
padding.bottom - padding.bottom -
kBottomNavigationBarHeight - kBottomNavigationBarHeight -
kToolbarHeight; kToolbarHeight;
final maxHeight = highlights != null final maxHeight =
? min(600.0, availableHeight) highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight);
: min(500.0, availableHeight);
return Card( return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null, color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
child: InkWell( child: InkWell(

View File

@@ -25,6 +25,7 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; 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/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';
@@ -185,6 +186,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userId = context.watch<AuthenticationCubit>().state.userId;
final destinations = [ final destinations = [
RouteDescription( RouteDescription(
icon: const Icon(Icons.description_outlined), icon: const Icon(Icons.description_outlined),
@@ -232,19 +234,20 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
]; ];
final routes = <Widget>[ final routes = <Widget>[
MultiBlocProvider( MultiBlocProvider(
key: ValueKey(userId),
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => DocumentsCubit( create: (context) => DocumentsCubit(
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
), )..reload(),
), ),
BlocProvider( BlocProvider(
create: (context) => SavedViewCubit( create: (context) => SavedViewCubit(
context.read(), context.read(),
context.read(), context.read(),
), )..reload(),
), ),
], ],
child: const DocumentsPage(), child: const DocumentsPage(),
@@ -254,6 +257,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
child: const ScannerPage(), child: const ScannerPage(),
), ),
MultiBlocProvider( MultiBlocProvider(
key: ValueKey(userId),
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => LabelCubit(context.read()), create: (context) => LabelCubit(context.read()),
@@ -266,12 +270,12 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
child: const InboxPage(), child: const InboxPage(),
), ),
]; ];
return MultiBlocListener( return MultiBlocListener(
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) => listenWhen: (previous, current) => current == ConnectivityState.connected,
current == ConnectivityState.connected,
listener: (context, state) { listener: (context, state) {
_initializeData(context); _initializeData(context);
}, },
@@ -280,9 +284,7 @@ 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 context.read<LocalNotificationService>().notifyTaskChanged(state.task!);
.read<LocalNotificationService>()
.notifyTaskChanged(state.task!);
} }
}, },
), ),
@@ -295,9 +297,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
children: [ children: [
NavigationRail( NavigationRail(
labelType: NavigationRailLabelType.all, labelType: NavigationRailLabelType.all,
destinations: destinations destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(),
.map((e) => e.toNavigationRailDestination())
.toList(),
selectedIndex: _currentIndex, selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged, onDestinationSelected: _onNavigationChanged,
), ),
@@ -315,8 +315,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
elevation: 4.0, elevation: 4.0,
selectedIndex: _currentIndex, selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged, onDestinationSelected: _onNavigationChanged,
destinations: destinations: destinations.map((e) => e.toNavigationDestination()).toList(),
destinations.map((e) => e.toNavigationDestination()).toList(),
), ),
body: routes[_currentIndex], body: routes[_currentIndex],
); );

View File

@@ -5,9 +5,8 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.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/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -33,9 +32,7 @@ class VerifyIdentityPage extends StatelessWidget {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(S Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16), .paddedSymmetrically(horizontal: 16),
const Icon( const Icon(
Icons.fingerprint, Icons.fingerprint,
@@ -57,9 +54,7 @@ class VerifyIdentityPage extends StatelessWidget {
), ),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => context onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
.read<AuthenticationCubit>()
.restoreSessionState(),
child: Text(S.of(context)!.verifyIdentity), child: Text(S.of(context)!.verifyIdentity),
), ),
], ],

View File

@@ -13,8 +13,7 @@ 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> class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin {
with DocumentPagingBlocMixin {
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
final PaperlessDocumentsApi _documentsApi; final PaperlessDocumentsApi _documentsApi;
@@ -37,10 +36,7 @@ class InboxCubit extends HydratedCubit<InboxState>
this, this,
onDeleted: remove, onDeleted: remove,
onUpdated: (document) { onUpdated: (document) {
if (document.tags if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) {
.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 {
@@ -76,7 +72,9 @@ class InboxCubit extends HydratedCubit<InboxState>
/// Fetches inbox tag ids and loads the inbox items (documents). /// Fetches inbox tag ids and loads the inbox items (documents).
/// ///
Future<void> loadInbox() async { Future<void> loadInbox() async {
if (!isClosed) {
debugPrint("Initializing inbox..."); debugPrint("Initializing inbox...");
final inboxTags = await _labelRepository.findAllTags().then( final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!), (tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
); );
@@ -91,6 +89,7 @@ class InboxCubit extends HydratedCubit<InboxState>
), ),
); );
} }
emit(state.copyWith(inboxTags: inboxTags)); emit(state.copyWith(inboxTags: inboxTags));
updateFilter( updateFilter(
filter: DocumentFilter( filter: DocumentFilter(
@@ -99,6 +98,7 @@ class InboxCubit extends HydratedCubit<InboxState>
), ),
); );
} }
}
/// ///
/// Fetches inbox tag ids and loads the inbox items (documents). /// Fetches inbox tag ids and loads the inbox items (documents).
@@ -133,8 +133,7 @@ class InboxCubit extends HydratedCubit<InboxState>
/// from the inbox. /// from the inbox.
/// ///
Future<Iterable<int>> removeFromInbox(DocumentModel document) async { Future<Iterable<int>> removeFromInbox(DocumentModel document) async {
final tagsToRemove = final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet());
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(
@@ -188,8 +187,8 @@ class InboxCubit extends HydratedCubit<InboxState>
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 = await _documentsApi final updatedDocument =
.update(document.copyWith(archiveSerialNumber: () => asn)); await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn));
replace(updatedDocument); replace(updatedDocument);
} }

View File

@@ -1,18 +1,23 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.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/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.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/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.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_credentials.model.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/user_settings.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
part 'authentication_state.dart'; part 'authentication_state.dart';
@@ -20,15 +25,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService; final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi; final PaperlessAuthenticationApi _authApi;
final SessionManager _dioWrapper; final SessionManager _dioWrapper;
final LabelRepository _labelRepository;
final SavedViewRepository _savedViewRepository;
final PaperlessServerStatsApi _serverStatsApi;
AuthenticationCubit( AuthenticationCubit(
this._localAuthService, this._localAuthService,
this._authApi, this._authApi,
this._dioWrapper, this._dioWrapper,
) : super(AuthenticationState.initial); this._labelRepository,
this._savedViewRepository,
this._serverStatsApi,
) : super(const AuthenticationState());
Future<void> login({ Future<void> login({
required UserCredentials credentials, required LoginFormCredentials credentials,
required String serverUrl, required String serverUrl,
ClientCertificate? clientCertificate, ClientCertificate? clientCertificate,
}) async { }) async {
@@ -47,107 +58,239 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCertificate, clientCertificate: clientCertificate,
authToken: token, authToken: token,
); );
final authInfo = AuthenticationInformation(
username: credentials.username!,
serverUrl: serverUrl,
clientCertificate: clientCertificate,
token: token,
);
final userId = "${credentials.username}@$serverUrl"; final userId = "${credentials.username}@$serverUrl";
// Mark logged in user as currently active user. // If it is first time login, create settings for this user.
final globalSettings = GlobalAppSettings.boxedValue; final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
globalSettings.currentLoggedInUser = userId; final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
await globalSettings.save(); if (!userSettingsBox.containsKey(userId)) {
userSettingsBox.put(userId, UserSettings());
}
final fullName = await _fetchFullName();
// Save credentials in encrypted box if (!userAccountBox.containsKey(userId)) {
final encryptedBox = await _openEncryptedBox(); userAccountBox.put(
await encryptedBox.put(
userId, userId,
authInfo, UserAccount(
); serverUrl: serverUrl,
encryptedBox.close(); username: credentials.username!,
fullName: fullName,
emit(
AuthenticationState(
wasLoginStored: false,
authentication: authInfo,
), ),
); );
} }
// Mark logged in user as currently active user.
final globalSettings = GlobalSettings.boxedValue;
globalSettings.currentLoggedInUser = userId;
globalSettings.save();
// Save credentials in encrypted box
final userCredentialsBox = await _getUserCredentialsBox();
await userCredentialsBox.put(
userId,
UserCredentials(
token: token,
clientCertificate: clientCertificate,
),
);
userCredentialsBox.close();
emit(
AuthenticationState(
isAuthenticated: true,
username: credentials.username,
userId: userId,
fullName: fullName,
//TODO: Query ui settings with full name and add as parameter here...
),
);
}
/// Switches to another account if it exists.
Future<void> switchAccount(String userId) async {
final globalSettings = GlobalSettings.boxedValue;
if (globalSettings.currentLoggedInUser == userId) {
return;
}
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
if (!userSettingsBox.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 (!authenticated) {
debugPrint("User unable to authenticate.");
return;
}
}
final credentialsBox = await _getUserCredentialsBox();
if (!credentialsBox.containsKey(userId)) {
await credentialsBox.close();
debugPrint("Invalid authentication for $userId");
return;
}
final credentials = credentialsBox.get(userId);
await _resetExternalState();
_dioWrapper.updateSettings(
authToken: credentials!.token,
clientCertificate: credentials.clientCertificate,
serverInformation: PaperlessServerInformationModel(),
baseUrl: account.serverUrl,
);
globalSettings.currentLoggedInUser = userId;
await globalSettings.save();
await _reloadRepositories();
emit(
AuthenticationState(
isAuthenticated: true,
username: account.username,
fullName: account.fullName,
userId: userId,
),
);
}
Future<String> addAccount({
required LoginFormCredentials credentials,
required String serverUrl,
ClientCertificate? clientCertificate,
required bool enableBiometricAuthentication,
}) async {
assert(credentials.password != null && credentials.username != null);
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");
}
// Creates a parallel session to get token and disposes of resources after.
final sessionManager = SessionManager([
DioHttpErrorInterceptor(),
]);
sessionManager.updateSettings(
clientCertificate: clientCertificate,
baseUrl: serverUrl,
);
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
final token = await authApi.login(
username: credentials.username!,
password: credentials.password!,
);
sessionManager.resetSettings();
await userSettingsBox.put(
userId,
UserSettings(
isBiometricAuthenticationEnabled: enableBiometricAuthentication,
),
);
final fullName = await _fetchFullName();
await userAccountsBox.put(
userId,
UserAccount(
serverUrl: serverUrl,
username: credentials.username!,
fullName: fullName,
),
);
final userCredentialsBox = await _getUserCredentialsBox();
await userCredentialsBox.put(
userId,
UserCredentials(
token: token,
clientCertificate: clientCertificate,
),
);
await userCredentialsBox.close();
return userId;
}
Future<void> removeAccount(String userId) async {
final globalSettings = GlobalSettings.boxedValue;
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();
}
}
/// ///
/// 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 = GlobalAppSettings.boxedValue; final globalSettings = GlobalSettings.boxedValue;
if (globalSettings.currentLoggedInUser == null) { final userId = globalSettings.currentLoggedInUser;
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<UserAppSettings>(HiveBoxes.userSettings) final userSettings = Hive.box<UserSettings>(HiveBoxes.userSettings).get(userId)!;
.get(globalSettings.currentLoggedInUser!); final userAccount = Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!;
if (userSettings!.isBiometricAuthenticationEnabled) { if (userSettings.isBiometricAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService final localAuthSuccess =
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (localAuthSuccess) { if (!localAuthSuccess) {
final authentication = await _readAuthenticationFromEncryptedBox( emit(const AuthenticationState(showBiometricAuthenticationScreen: true));
globalSettings.currentLoggedInUser!); return;
}
}
final userCredentialsBox = await _getUserCredentialsBox();
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!);
if (authentication != null) { if (authentication != null) {
_dioWrapper.updateSettings( _dioWrapper.updateSettings(
clientCertificate: authentication.clientCertificate, clientCertificate: authentication.clientCertificate,
authToken: authentication.token, authToken: authentication.token,
baseUrl: authentication.serverUrl, baseUrl: userAccount.serverUrl,
); serverInformation: PaperlessServerInformationModel(),
return emit(
AuthenticationState(
wasLoginStored: true,
authentication: state.authentication,
wasLocalAuthenticationSuccessful: true,
),
);
}
} else {
return emit(
AuthenticationState(
wasLoginStored: true,
wasLocalAuthenticationSuccessful: false,
authentication: null,
),
);
}
} else {
final authentication = await _readAuthenticationFromEncryptedBox(
globalSettings.currentLoggedInUser!);
if (authentication != null) {
_dioWrapper.updateSettings(
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: authentication.serverUrl,
); );
emit( emit(
AuthenticationState( AuthenticationState(
authentication: authentication, isAuthenticated: true,
wasLoginStored: true, showBiometricAuthenticationScreen: false,
username: userAccount.username,
), ),
); );
} else { } else {
return emit(AuthenticationState.initial); throw Exception("User should be authenticated but no authentication information was found.");
}
} }
} }
Future<AuthenticationInformation?> _readAuthenticationFromEncryptedBox( Future<void> logout() async {
String userId) { await _resetExternalState();
return _openEncryptedBox().then((box) => box.get(userId)); final globalSettings = GlobalSettings.boxedValue;
globalSettings
..currentLoggedInUser = null
..save();
emit(const AuthenticationState());
} }
Future<Box<AuthenticationInformation?>> _openEncryptedBox() async { Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage(); const secureStorage = FlutterSecureStorage();
final encryptionKeyString = await secureStorage.read(key: 'key'); if (!await secureStorage.containsKey(key: 'key')) {
if (encryptionKeyString == null) {
final key = Hive.generateSecureKey(); final key = Hive.generateSecureKey();
await secureStorage.write( await secureStorage.write(
@@ -155,17 +298,40 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
value: base64UrlEncode(key), value: base64UrlEncode(key),
); );
} }
final key = await secureStorage.read(key: 'key'); final key = (await secureStorage.read(key: 'key'))!;
final encryptionKeyUint8List = base64Url.decode(key!); return base64Decode(key);
return await Hive.openBox<AuthenticationInformation>( }
HiveBoxes.vault,
encryptionCipher: HiveAesCipher(encryptionKeyUint8List), Future<Box<UserCredentials>> _getUserCredentialsBox() async {
final keyBytes = await _getEncryptedBoxKey();
return Hive.openBox<UserCredentials>(
HiveBoxes.userCredentials,
encryptionCipher: HiveAesCipher(keyBytes),
); );
} }
Future<void> logout() async { Future<void> _resetExternalState() {
await Hive.box<AuthenticationInformation>(HiveBoxes.authentication).clear();
_dioWrapper.resetSettings(); _dioWrapper.resetSettings();
emit(AuthenticationState.initial); return Future.wait([
HydratedBloc.storage.clear(),
_labelRepository.clear(),
_savedViewRepository.clear(),
]);
}
Future<void> _reloadRepositories() {
return Future.wait([
_labelRepository.initialize(),
_savedViewRepository.findAll(),
]);
}
Future<String?> _fetchFullName() async {
try {
final uiSettings = await _serverStatsApi.getUiSettings();
return uiSettings.displayName;
} catch (error) {
return null;
}
} }
} }

View File

@@ -1,35 +1,43 @@
part of 'authentication_cubit.dart'; part of 'authentication_cubit.dart';
@JsonSerializable() class AuthenticationState with EquatableMixin {
class AuthenticationState { final bool showBiometricAuthenticationScreen;
final bool wasLoginStored; final bool isAuthenticated;
@JsonKey(includeFromJson: false, includeToJson: false) final String? username;
final bool? wasLocalAuthenticationSuccessful; final String? fullName;
final AuthenticationInformation? authentication; final String? userId;
static final AuthenticationState initial = AuthenticationState( const AuthenticationState({
wasLoginStored: false, this.isAuthenticated = false,
); this.showBiometricAuthenticationScreen = false,
this.username,
bool get isAuthenticated => authentication != null; this.fullName,
this.userId,
AuthenticationState({
required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
this.authentication,
}); });
AuthenticationState copyWith({ AuthenticationState copyWith({
bool? wasLoginStored,
bool? isAuthenticated, bool? isAuthenticated,
AuthenticationInformation? authentication, bool? showBiometricAuthenticationScreen,
bool? wasLocalAuthenticationSuccessful, String? username,
String? fullName,
String? userId,
}) { }) {
return AuthenticationState( return AuthenticationState(
wasLoginStored: wasLoginStored ?? this.wasLoginStored, isAuthenticated: isAuthenticated ?? this.isAuthenticated,
authentication: authentication ?? this.authentication, showBiometricAuthenticationScreen:
wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ?? showBiometricAuthenticationScreen ?? this.showBiometricAuthenticationScreen,
this.wasLocalAuthenticationSuccessful, username: username ?? this.username,
fullName: fullName ?? this.fullName,
userId: userId ?? this.userId,
); );
} }
@override
List<Object?> get props => [
userId,
username,
fullName,
isAuthenticated,
showBiometricAuthenticationScreen,
];
} }

View File

@@ -3,44 +3,15 @@ import 'dart:typed_data';
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/core/type/types.dart';
part 'client_certificate.g.dart'; part 'client_certificate.g.dart';
@HiveType(typeId: HiveTypeIds.clientCertificate) @HiveType(typeId: HiveTypeIds.clientCertificate)
class ClientCertificate { class ClientCertificate {
static const bytesKey = 'bytes';
static const passphraseKey = 'passphrase';
@HiveField(0) @HiveField(0)
final Uint8List bytes; Uint8List bytes;
@HiveField(1) @HiveField(1)
final String? passphrase; String? passphrase;
ClientCertificate({required this.bytes, this.passphrase}); ClientCertificate({required this.bytes, this.passphrase});
static ClientCertificate? nullable(Uint8List? bytes, {String? passphrase}) {
if (bytes != null) {
return ClientCertificate(bytes: bytes, passphrase: passphrase);
}
return null;
}
JSON toJson() {
return {
bytesKey: base64Encode(bytes),
passphraseKey: passphrase,
};
}
ClientCertificate.fromJson(JSON json)
: bytes = base64Decode(json[bytesKey]),
passphrase = json[passphraseKey];
ClientCertificate copyWith({Uint8List? bytes, String? passphrase}) {
return ClientCertificate(
bytes: bytes ?? this.bytes,
passphrase: passphrase ?? this.passphrase,
);
}
} }

View File

@@ -0,0 +1,19 @@
import 'dart:convert';
import 'dart:typed_data';
class ClientCertificateFormModel {
static const bytesKey = 'bytes';
static const passphraseKey = 'passphrase';
final Uint8List bytes;
final String? passphrase;
ClientCertificateFormModel({required this.bytes, this.passphrase});
ClientCertificateFormModel copyWith({Uint8List? bytes, String? passphrase}) {
return ClientCertificateFormModel(
bytes: bytes ?? this.bytes,
passphrase: passphrase ?? this.passphrase,
);
}
}

View File

@@ -0,0 +1,13 @@
class LoginFormCredentials {
final String? username;
final String? password;
LoginFormCredentials({this.username, this.password});
LoginFormCredentials copyWith({String? username, String? password}) {
return LoginFormCredentials(
username: username ?? this.username,
password: password ?? this.password,
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
part 'user_account.g.dart';
@HiveType(typeId: HiveTypeIds.userAccount)
class UserAccount {
@HiveField(0)
final String serverUrl;
@HiveField(1)
final String username;
@HiveField(2)
final String? fullName;
UserAccount({
required this.serverUrl,
required this.username,
this.fullName,
});
}

View File

@@ -0,0 +1,18 @@
import 'package:hive/hive.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
part 'user_credentials.g.dart';
@HiveType(typeId: HiveTypeIds.userCredentials)
class UserCredentials extends HiveObject {
@HiveField(0)
final String token;
@HiveField(1)
final ClientCertificate? clientCertificate;
UserCredentials({
required this.token,
this.clientCertificate,
});
}

View File

@@ -1,13 +0,0 @@
class UserCredentials {
final String? username;
final String? password;
UserCredentials({this.username, this.password});
UserCredentials copyWith({String? username, String? password}) {
return UserCredentials(
username: username ?? this.username,
password: password ?? this.password,
);
}
}

View File

@@ -3,18 +3,37 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.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/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'widgets/login_pages/server_login_page.dart'; import 'widgets/login_pages/server_login_page.dart';
import 'widgets/never_scrollable_scroll_behavior.dart'; import 'widgets/never_scrollable_scroll_behavior.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key); final void Function(
BuildContext context,
String username,
String password,
String serverUrl,
ClientCertificate? clientCertificate,
) onSubmit;
final String submitText;
const LoginPage({
Key? key,
required this.onSubmit,
required this.submitText,
}) : super(key: key);
@override @override
State<LoginPage> createState() => _LoginPageState(); State<LoginPage> createState() => _LoginPageState();
@@ -46,7 +65,8 @@ class _LoginPageState extends State<LoginPage> {
), ),
ServerLoginPage( ServerLoginPage(
formBuilderKey: _formKey, formBuilderKey: _formKey,
onDone: _login, submitText: widget.submitText,
onSubmit: _login,
), ),
], ],
), ),
@@ -58,24 +78,23 @@ class _LoginPageState extends State<LoginPage> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value; final form = _formKey.currentState!.value;
try { ClientCertificate? clientCert;
await context.read<AuthenticationCubit>().login( final clientCertFormModel =
credentials: form[UserCredentialsFormField.fkCredentials], form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?;
serverUrl: form[ServerAddressFormField.fkServerAddress], if (clientCertFormModel != null) {
clientCertificate: clientCert = ClientCertificate(
form[ClientCertificateFormField.fkClientCertificate], bytes: clientCertFormModel.bytes,
passphrase: clientCertFormModel.passphrase,
); );
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (error, stackTrace) {
if (error.hasFieldUnspecificError) {
showLocalizedError(context, error.fieldUnspecificError!);
} else {
showGenericError(context, error.values.first, stackTrace);
}
} catch (unknownError, stackTrace) {
showGenericError(context, unknownError.toString(), stackTrace);
} }
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
widget.onSubmit(
context,
credentials.username!,
credentials.password!,
form[ServerAddressFormField.fkServerAddress],
clientCert,
);
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.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/client_certificate_form_model.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
@@ -15,23 +16,21 @@ import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget { class ClientCertificateFormField extends StatefulWidget {
static const fkClientCertificate = 'clientCertificate'; static const fkClientCertificate = 'clientCertificate';
final void Function(ClientCertificate? cert) onChanged; final void Function(ClientCertificateFormModel? cert) onChanged;
const ClientCertificateFormField({ const ClientCertificateFormField({
Key? key, Key? key,
required this.onChanged, required this.onChanged,
}) : super(key: key); }) : super(key: key);
@override @override
State<ClientCertificateFormField> createState() => State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState();
_ClientCertificateFormFieldState();
} }
class _ClientCertificateFormFieldState class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> {
extends State<ClientCertificateFormField> {
File? _selectedFile; File? _selectedFile;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FormBuilderField<ClientCertificate?>( return FormBuilderField<ClientCertificateFormModel?>(
key: const ValueKey('login-client-cert'), key: const ValueKey('login-client-cert'),
onChanged: widget.onChanged, onChanged: widget.onChanged,
initialValue: null, initialValue: null,
@@ -46,8 +45,7 @@ class _ClientCertificateFormFieldState
return null; return null;
}, },
builder: (field) { builder: (field) {
final theme = final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
return Theme( return Theme(
data: theme, data: theme,
child: ExpansionTile( child: ExpansionTile(
@@ -124,7 +122,7 @@ class _ClientCertificateFormFieldState
); );
} }
Future<void> _onSelectFile(FormFieldState<ClientCertificate?> field) async { Future<void> _onSelectFile(FormFieldState<ClientCertificateFormModel?> field) async {
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: false, allowMultiple: false,
); );
@@ -133,14 +131,13 @@ class _ClientCertificateFormFieldState
setState(() { setState(() {
_selectedFile = file; _selectedFile = file;
}); });
final changedValue = final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ??
field.value?.copyWith(bytes: file.readAsBytesSync()) ?? ClientCertificateFormModel(bytes: file.readAsBytesSync());
ClientCertificate(bytes: file.readAsBytesSync());
field.didChange(changedValue); field.didChange(changedValue);
} }
} }
Widget _buildSelectedFileText(FormFieldState<ClientCertificate?> field) { Widget _buildSelectedFileText(FormFieldState<ClientCertificateFormModel?> field) {
if (field.value == null) { if (field.value == null) {
assert(_selectedFile == null); assert(_selectedFile == null);
return Text( return Text(

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -14,14 +14,13 @@ class UserCredentialsFormField extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<UserCredentialsFormField> createState() => State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState();
_UserCredentialsFormFieldState();
} }
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> { class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FormBuilderField<UserCredentials?>( return FormBuilderField<LoginFormCredentials?>(
name: UserCredentialsFormField.fkCredentials, name: UserCredentialsFormField.fkCredentials,
builder: (field) => AutofillGroup( builder: (field) => AutofillGroup(
child: Column( child: Column(
@@ -34,7 +33,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
autocorrect: false, autocorrect: false,
onChanged: (username) => field.didChange( onChanged: (username) => field.didChange(
field.value?.copyWith(username: username) ?? field.value?.copyWith(username: username) ??
UserCredentials(username: username), LoginFormCredentials(username: username),
), ),
validator: (value) { validator: (value) {
if (value?.trim().isEmpty ?? true) { if (value?.trim().isEmpty ?? true) {
@@ -51,7 +50,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
label: S.of(context)!.password, label: S.of(context)!.password,
onChanged: (password) => field.didChange( onChanged: (password) => field.didChange(
field.value?.copyWith(password: password) ?? field.value?.copyWith(password: password) ??
UserCredentials(password: password), LoginFormCredentials(password: password),
), ),
validator: (value) { validator: (value) {
if (value?.trim().isEmpty ?? true) { if (value?.trim().isEmpty ?? true) {

View File

@@ -3,6 +3,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
@@ -35,9 +37,8 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
toolbarHeight: kToolbarHeight - 4, toolbarHeight: kToolbarHeight - 4,
title: Text(S.of(context)!.connectToPaperless), title: Text(S.of(context)!.connectToPaperless),
bottom: PreferredSize( bottom: PreferredSize(
child: _isCheckingConnection child:
? const LinearProgressIndicator() _isCheckingConnection ? const LinearProgressIndicator() : const SizedBox(height: 4.0),
: const SizedBox(height: 4.0),
preferredSize: const Size.fromHeight(4.0), preferredSize: const Size.fromHeight(4.0),
), ),
), ),
@@ -67,9 +68,8 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
), ),
FilledButton( FilledButton(
child: Text(S.of(context)!.continueLabel), child: Text(S.of(context)!.continueLabel),
onPressed: _reachabilityStatus == ReachabilityStatus.reachable onPressed:
? widget.onContinue _reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null,
: null,
), ),
], ],
), ),
@@ -81,16 +81,16 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
setState(() { setState(() {
_isCheckingConnection = true; _isCheckingConnection = true;
}); });
final certForm = widget.formBuilderKey.currentState
final status = await context ?.getRawValue(ClientCertificateFormField.fkClientCertificate)
.read<ConnectivityStatusService>() as ClientCertificateFormModel?;
.isPaperlessServerReachable( final status = await context.read<ConnectivityStatusService>().isPaperlessServerReachable(
address ?? address ??
widget.formBuilderKey.currentState! widget.formBuilderKey.currentState!
.getRawValue(ServerAddressFormField.fkServerAddress), .getRawValue(ServerAddressFormField.fkServerAddress),
widget.formBuilderKey.currentState?.getRawValue( certForm != null
ClientCertificateFormField.fkClientCertificate, ? ClientCertificate(bytes: certForm.bytes, passphrase: certForm.passphrase)
), : null,
); );
setState(() { setState(() {
_isCheckingConnection = false; _isCheckingConnection = false;

View File

@@ -6,12 +6,14 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class ServerLoginPage extends StatefulWidget { class ServerLoginPage extends StatefulWidget {
final Future<void> Function() onDone; final String submitText;
final Future<void> Function() onSubmit;
final GlobalKey<FormBuilderState> formBuilderKey; final GlobalKey<FormBuilderState> formBuilderKey;
const ServerLoginPage({ const ServerLoginPage({
super.key, super.key,
required this.onDone, required this.onSubmit,
required this.formBuilderKey, required this.formBuilderKey,
required this.submitText,
}); });
@override @override
@@ -23,8 +25,7 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serverAddress = (widget.formBuilderKey.currentState final serverAddress = (widget.formBuilderKey.currentState
?.getRawValue(ServerAddressFormField.fkServerAddress) ?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
as String?)
?.replaceAll(RegExp(r'https?://'), '') ?? ?.replaceAll(RegExp(r'https?://'), '') ??
''; '';
return Scaffold( return Scaffold(
@@ -50,7 +51,7 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
setState(() => _isLoginLoading = true); setState(() => _isLoginLoading = true);
await widget.onDone(); await widget.onSubmit();
setState(() => _isLoginLoading = false); setState(() => _isLoginLoading = false);
}, },
child: Text(S.of(context)!.signIn), child: Text(S.of(context)!.signIn),

View File

@@ -43,12 +43,14 @@ class SavedViewDetailsCubit extends HydratedCubit<SavedViewDetailsState>
_labelRepository.addListener( _labelRepository.addListener(
this, this,
onChanged: (labels) { onChanged: (labels) {
if (!isClosed) {
emit(state.copyWith( emit(state.copyWith(
correspondents: labels.correspondents, correspondents: labels.correspondents,
documentTypes: labels.documentTypes, documentTypes: labels.documentTypes,
tags: labels.tags, tags: labels.tags,
storagePaths: labels.storagePaths, storagePaths: labels.storagePaths,
)); ));
}
}, },
); );
updateFilter(filter: savedView.toDocumentFilter()); updateFilter(filter: savedView.toDocumentFilter());

View File

@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.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/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
as s;
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.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';
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
typedef OpenSearchCallback = void Function(BuildContext context); typedef OpenSearchCallback = void Function(BuildContext context);
@@ -47,8 +47,7 @@ class _SearchAppBarState extends State<SearchAppBar> {
onPressed: Scaffold.of(context).openDrawer, onPressed: Scaffold.of(context).openDrawer,
), ),
trailingIcon: IconButton( trailingIcon: IconButton(
icon: BlocBuilder<PaperlessServerInformationCubit, icon: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
PaperlessServerInformationState>(
builder: (context, state) { builder: (context, state) {
return CircleAvatar( return CircleAvatar(
child: Text(state.information?.userInitials ?? ''), child: Text(state.information?.userInitials ?? ''),
@@ -58,7 +57,10 @@ class _SearchAppBarState extends State<SearchAppBar> {
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) => const AccountSettingsDialog(), builder: (context) => BlocProvider.value(
value: context.read<PaperlessServerInformationCubit>(),
child: const ManageAccountsPage(),
),
); );
}, },
), ),

View File

@@ -1,64 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
part 'application_settings_cubit.g.dart';
part 'application_settings_state.dart';
class ApplicationSettingsCubit extends HydratedCubit<ApplicationSettingsState> {
final LocalAuthenticationService _localAuthenticationService;
ApplicationSettingsCubit(this._localAuthenticationService)
: super(ApplicationSettingsState.defaultSettings);
Future<void> setLocale(String? localeSubtag) async {
final updatedSettings = state.copyWith(preferredLocaleSubtag: localeSubtag);
_updateSettings(updatedSettings);
}
Future<void> setIsBiometricAuthenticationEnabled(
bool isEnabled, {
required String localizedReason,
}) async {
final isActionAuthorized = await _localAuthenticationService
.authenticateLocalUser(localizedReason);
if (isActionAuthorized) {
final updatedSettings =
state.copyWith(isLocalAuthenticationEnabled: isEnabled);
_updateSettings(updatedSettings);
}
}
void setThemeMode(ThemeMode? selectedMode) {
final updatedSettings = state.copyWith(preferredThemeMode: selectedMode);
_updateSettings(updatedSettings);
}
void setColorSchemeOption(ColorSchemeOption schemeOption) {
final updatedSettings =
state.copyWith(preferredColorSchemeOption: schemeOption);
_updateSettings(updatedSettings);
}
void _updateSettings(ApplicationSettingsState settings) async {
emit(settings);
}
@override
Future<void> clear() async {
await super.clear();
emit(ApplicationSettingsState.defaultSettings);
}
@override
ApplicationSettingsState? fromJson(Map<String, dynamic> json) =>
ApplicationSettingsState.fromJson(json);
@override
Map<String, dynamic>? toJson(ApplicationSettingsState state) =>
state.toJson();
}

View File

@@ -1,53 +0,0 @@
part of 'application_settings_cubit.dart';
///
/// State holding the current application settings such as selected language, theme mode and more.
///
@JsonSerializable()
class ApplicationSettingsState {
static final defaultSettings = ApplicationSettingsState(
preferredLocaleSubtag: _defaultPreferredLocaleSubtag,
);
final bool isLocalAuthenticationEnabled;
final String preferredLocaleSubtag;
final ThemeMode preferredThemeMode;
final ColorSchemeOption preferredColorSchemeOption;
ApplicationSettingsState({
required this.preferredLocaleSubtag,
this.preferredThemeMode = ThemeMode.system,
this.isLocalAuthenticationEnabled = false,
this.preferredColorSchemeOption = ColorSchemeOption.classic,
});
Map<String, dynamic> toJson() => _$ApplicationSettingsStateToJson(this);
factory ApplicationSettingsState.fromJson(Map<String, dynamic> json) =>
_$ApplicationSettingsStateFromJson(json);
ApplicationSettingsState copyWith({
bool? isLocalAuthenticationEnabled,
String? preferredLocaleSubtag,
ThemeMode? preferredThemeMode,
ColorSchemeOption? preferredColorSchemeOption,
}) {
return ApplicationSettingsState(
isLocalAuthenticationEnabled:
isLocalAuthenticationEnabled ?? this.isLocalAuthenticationEnabled,
preferredLocaleSubtag:
preferredLocaleSubtag ?? this.preferredLocaleSubtag,
preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode,
preferredColorSchemeOption:
preferredColorSchemeOption ?? this.preferredColorSchemeOption,
);
}
static String get _defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first;
if (!S.supportedLocales
.any((locale) => locale.languageCode == preferredLocale)) {
preferredLocale = 'en';
}
return preferredLocale;
}
}

View File

@@ -3,10 +3,10 @@ import 'package:hive/hive.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/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
part 'global_app_settings.g.dart'; part 'global_settings.g.dart';
@HiveType(typeId: HiveTypeIds.globalSettings) @HiveType(typeId: HiveTypeIds.globalSettings)
class GlobalAppSettings with ChangeNotifier, HiveObjectMixin { class GlobalSettings with HiveObjectMixin {
@HiveField(0) @HiveField(0)
String preferredLocaleSubtag; String preferredLocaleSubtag;
@@ -22,7 +22,7 @@ class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
@HiveField(4) @HiveField(4)
String? currentLoggedInUser; String? currentLoggedInUser;
GlobalAppSettings({ GlobalSettings({
required this.preferredLocaleSubtag, required this.preferredLocaleSubtag,
this.preferredThemeMode = ThemeMode.system, this.preferredThemeMode = ThemeMode.system,
this.preferredColorSchemeOption = ColorSchemeOption.classic, this.preferredColorSchemeOption = ColorSchemeOption.classic,
@@ -30,7 +30,6 @@ class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
this.currentLoggedInUser, this.currentLoggedInUser,
}); });
static GlobalAppSettings get boxedValue => static GlobalSettings get boxedValue =>
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings) Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
.get(HiveBoxSingleValueKey.value)!;
} }

View File

@@ -1,16 +1,14 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.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/color_scheme_option.dart';
part 'user_app_settings.g.dart'; part 'user_settings.g.dart';
@HiveType(typeId: HiveTypeIds.userSettings) @HiveType(typeId: HiveTypeIds.userSettings)
class UserAppSettings with HiveObjectMixin { class UserSettings with HiveObjectMixin {
@HiveField(0) @HiveField(0)
bool isBiometricAuthenticationEnabled; bool isBiometricAuthenticationEnabled;
UserAppSettings({ UserSettings({
this.isBiometricAuthenticationEnabled = false, this.isBiometricAuthenticationEnabled = false,
}); });
} }

View File

@@ -1,18 +1,18 @@
import 'package:collection/collection.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:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; 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/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
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/repository/label_repository.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.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/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
class AccountSettingsDialog extends StatelessWidget { class AccountSettingsDialog extends StatelessWidget {
@@ -20,7 +20,9 @@ class AccountSettingsDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GlobalSettingsBuilder(builder: (context, globalSettings) {
return AlertDialog( return AlertDialog(
insetPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 32),
scrollable: true, scrollable: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Row( title: Row(
@@ -30,24 +32,29 @@ class AccountSettingsDialog extends StatelessWidget {
const CloseButton(), const CloseButton(),
], ],
), ),
content: BlocBuilder<PaperlessServerInformationCubit, content: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
PaperlessServerInformationState>(
builder: (context, state) { builder: (context, state) {
return Column( return Column(
children: [ children: [
ExpansionTile( ValueListenableBuilder(
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
builder: (context, box, _) {
// final currentUser = globalSettings.currentLoggedInUser;
final currentUser = null;
final accountIds =
box.keys.whereNot((element) => element == currentUser).toList();
final accounts = accountIds.map((id) => box.get(id)!).toList();
return ExpansionTile(
leading: CircleAvatar( leading: CircleAvatar(
child: Text(state.information?.userInitials ?? ''), child: Text(state.information?.userInitials ?? ''),
), ),
title: Text(state.information?.username ?? ''), title: Text(state.information?.username ?? ''),
subtitle: Text(state.information?.host ?? ''), subtitle: Text(state.information?.host ?? ''),
children: const [ children:
HintCard( accounts.map((account) => _buildAccountTile(account, true)).toList(),
hintText: "WIP: Coming soon with multi user support!", );
},
), ),
],
),
const Divider(),
ListTile( ListTile(
dense: true, dense: true,
leading: const Icon(Icons.person_add_rounded), leading: const Icon(Icons.person_add_rounded),
@@ -77,17 +84,31 @@ class AccountSettingsDialog extends StatelessWidget {
}, },
), ),
); );
});
} }
Future<void> _onLogout(BuildContext context) async { Future<void> _onLogout(BuildContext context) async {
try { try {
await context.read<AuthenticationCubit>().logout(); await context.read<AuthenticationCubit>().logout();
await context.read<GlobalAppSettings>();
await context.read<LabelRepository>().clear();
await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear(); await HydratedBloc.storage.clear();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }
Widget _buildAccountTile(UserAccount account, bool isActive) {
return ListTile(
selected: isActive,
title: Text(account.username),
subtitle: Text(account.serverUrl),
leading: CircleAvatar(
child: Text((account.fullName ?? account.username)
.split(" ")
.take(2)
.map((e) => e.substring(0, 1))
.map((e) => e.toUpperCase())
.join(" ")),
),
);
}
} }

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class SwitchAccountDialog extends StatelessWidget {
final String username;
final String serverUrl;
const SwitchAccountDialog({
super.key,
required this.username,
required this.serverUrl,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Switch account"),
content: Text("Do you want to switch to $serverUrl and log in as $username?"),
actions: [
DialogConfirmButton(
style: DialogConfirmButtonStyle.danger,
label: S.of(context)!.continueLabel, //TODO: INTL change labels
),
DialogCancelButton(),
],
);
}
}

View File

@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.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';
class ManageAccountsPage extends StatelessWidget {
const ManageAccountsPage({super.key});
@override
Widget build(BuildContext context) {
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
leading: CloseButton(),
title: Text("Manage Accounts"), //TODO: INTL
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LoginPage(
onSubmit: (context, username, password, serverUrl, clientCertificate) async {
final userId = await context.read<AuthenticationCubit>().addAccount(
credentials: LoginFormCredentials(
username: username,
password: password,
),
clientCertificate: clientCertificate,
serverUrl: serverUrl,
//TODO: Ask user whether to enable biometric authentication
enableBiometricAuthentication: false,
);
final shoudSwitch = await showDialog(
context: context,
builder: (context) =>
SwitchAccountDialog(username: username, serverUrl: serverUrl),
) ??
false;
if (shoudSwitch) {
context.read<AuthenticationCubit>().switchAccount(userId);
}
},
submitText: "Add account",
),
),
);
},
label: Text("Add account"),
icon: Icon(Icons.person_add),
),
body: GlobalSettingsBuilder(
builder: (context, globalSettings) {
return ValueListenableBuilder(
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
builder: (context, box, _) {
final userIds = box.keys.toList().cast<String>();
return ListView.builder(
itemBuilder: (context, index) {
return _buildAccountTile(
context,
userIds[index],
box.get(userIds[index])!,
globalSettings,
);
},
itemCount: userIds.length,
);
},
);
},
),
),
);
}
Widget _buildAccountTile(
BuildContext context, String userId, UserAccount account, GlobalSettings settings) {
final theme = Theme.of(context);
return ListTile(
selected: userId == settings.currentLoggedInUser,
title: Text(account.username),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (account.fullName != null) Text(account.fullName!),
Text(account.serverUrl),
],
),
isThreeLine: true,
leading: CircleAvatar(
child: Text((account.fullName ?? account.username)
.split(" ")
.take(2)
.map((e) => e.substring(0, 1))
.map((e) => e.toUpperCase())
.join(" ")),
),
onTap: () async {
final navigator = Navigator.of(context);
if (settings.currentLoggedInUser == userId) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => SwitchingAccountsPage(),
),
);
await context.read<AuthenticationCubit>().switchAccount(userId);
navigator.popUntil((route) => route.isFirst);
},
trailing: TextButton(
child: Text(
"Remove",
style: TextStyle(
color: theme.colorScheme.error,
),
),
onPressed: () async {
final shouldPop = userId == settings.currentLoggedInUser;
await context.read<AuthenticationCubit>().removeAccount(userId);
if (shouldPop) {
Navigator.pop(context);
}
},
),
);
}
}

View File

@@ -0,0 +1,21 @@
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});
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: Material(
child: Center(
child: Text("Switching accounts. Please wait..."),
),
),
);
}
}

View File

@@ -2,12 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.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/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart'; import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart'; import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@@ -18,14 +15,13 @@ class SettingsPage extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text(S.of(context)!.settings), title: Text(S.of(context)!.settings),
), ),
bottomNavigationBar: BlocBuilder<PaperlessServerInformationCubit, bottomNavigationBar:
PaperlessServerInformationState>( BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
builder: (context, state) { builder: (context, state) {
final info = state.information!; final info = state.information!;
return ListTile( return ListTile(
title: Text( title: Text(
S.of(context)!.loggedInAs(info.username ?? 'unknown') + S.of(context)!.loggedInAs(info.username ?? 'unknown') + "@${info.host}",
"@${info.host}",
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),

View File

@@ -3,9 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.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/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/user_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@@ -7,8 +7,7 @@ import 'package:paperless_mobile/constants.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/translation/color_scheme_option_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/color_scheme_option.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/radio_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
@@ -37,8 +36,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
options: [ options: [
RadioOption( RadioOption(
value: ColorSchemeOption.classic, value: ColorSchemeOption.classic,
label: translateColorSchemeOption( label: translateColorSchemeOption(context, ColorSchemeOption.classic),
context, ColorSchemeOption.classic),
), ),
RadioOption( RadioOption(
value: ColorSchemeOption.dynamic, value: ColorSchemeOption.dynamic,
@@ -71,8 +69,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
bool _isBelowAndroid12() { bool _isBelowAndroid12() {
if (Platform.isAndroid) { if (Platform.isAndroid) {
final int version = final int version = int.tryParse(androidInfo!.version.release ?? '0') ?? 0;
int.tryParse(androidInfo!.version.release ?? '0') ?? 0;
return version < 12; return version < 12;
} }
return false; return false;

View File

@@ -3,21 +3,18 @@ import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart'; import 'package:flutter/src/widgets/placeholder.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/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
class GlobalSettingsBuilder extends StatelessWidget { class GlobalSettingsBuilder extends StatelessWidget {
final Widget Function(BuildContext context, GlobalSettings settings) builder;
final Widget Function(BuildContext context, GlobalAppSettings settings)
builder;
const GlobalSettingsBuilder({super.key, required this.builder}); const GlobalSettingsBuilder({super.key, required this.builder});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: valueListenable: Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings).listenable(),
builder: (context, value, _) { builder: (context, value, _) {
final settings = value.get(HiveBoxSingleValueKey.value)!; final settings = value.getValue()!;
return builder(context, settings); return builder(context, settings);
}, },
); );

View File

@@ -1,8 +1,7 @@
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_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.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';
@@ -11,8 +10,7 @@ class LanguageSelectionSetting extends StatefulWidget {
const LanguageSelectionSetting({super.key}); const LanguageSelectionSetting({super.key});
@override @override
State<LanguageSelectionSetting> createState() => State<LanguageSelectionSetting> createState() => _LanguageSelectionSettingState();
_LanguageSelectionSettingState();
} }
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> { class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {

View File

@@ -1,6 +1,5 @@
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:paperless_mobile/features/settings/cubit/application_settings_cubit.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/radio_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -14,8 +13,7 @@ class ThemeModeSetting extends StatelessWidget {
builder: (context, settings) { builder: (context, settings) {
return ListTile( return ListTile(
title: Text(S.of(context)!.appearance), title: Text(S.of(context)!.appearance),
subtitle: Text(_mapThemeModeToLocalizedString( subtitle: Text(_mapThemeModeToLocalizedString(settings.preferredThemeMode, context)),
settings.preferredThemeMode, context)),
onTap: () => showDialog<ThemeMode>( onTap: () => showDialog<ThemeMode>(
context: context, context: context,
builder: (_) => RadioSettingsDialog<ThemeMode>( builder: (_) => RadioSettingsDialog<ThemeMode>(

View File

@@ -1,13 +1,13 @@
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/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/user_settings.dart';
class UserSettingsBuilder extends StatelessWidget { class UserSettingsBuilder extends StatelessWidget {
final Widget Function( final Widget Function(
BuildContext context, BuildContext context,
UserAppSettings? settings, UserSettings? settings,
) builder; ) builder;
const UserSettingsBuilder({ const UserSettingsBuilder({
@@ -17,14 +17,11 @@ class UserSettingsBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder<Box<UserAppSettings>>( return ValueListenableBuilder<Box<UserSettings>>(
valueListenable: valueListenable: Hive.box<UserSettings>(HiveBoxes.userSettings).listenable(),
Hive.box<UserAppSettings>(HiveBoxes.userSettings).listenable(),
builder: (context, value, _) { builder: (context, value, _) {
final currentUser = final currentUser =
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings) Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
.get(HiveBoxSingleValueKey.value)
?.currentLoggedInUser;
if (currentUser != null) { if (currentUser != null) {
final settings = value.get(currentUser); final settings = value.get(currentUser);
return builder(context, settings); return builder(context, settings);

View File

@@ -49,7 +49,7 @@
}, },
"biometricAuthentication": "Biometric authentication", "biometricAuthentication": "Biometric authentication",
"@biometricAuthentication": {}, "@biometricAuthentication": {},
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}", "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
"@authenticateToToggleBiometricAuthentication": { "@authenticateToToggleBiometricAuthentication": {
"placeholders": { "placeholders": {
"mode": {} "mode": {}

View File

@@ -1,13 +1,13 @@
{ {
"developedBy": "Developed by {name}", "developedBy": "Stworzone przez {name}",
"@developedBy": { "@developedBy": {
"placeholders": { "placeholders": {
"name": {} "name": {}
} }
}, },
"addAnotherAccount": "Add another account", "addAnotherAccount": "Dodaj kolejne konto",
"@addAnotherAccount": {}, "@addAnotherAccount": {},
"account": "Account", "account": "Konto",
"@account": {}, "@account": {},
"addCorrespondent": "New Correspondent", "addCorrespondent": "New Correspondent",
"@addCorrespondent": { "@addCorrespondent": {
@@ -35,11 +35,11 @@
"name": {} "name": {}
} }
}, },
"disconnect": "Disconnect", "disconnect": "Rozłącz się",
"@disconnect": { "@disconnect": {
"description": "Logout button label" "description": "Logout button label"
}, },
"reportABug": "Report a Bug", "reportABug": "Zgłoś błąd",
"@reportABug": {}, "@reportABug": {},
"settings": "Ustawienia", "settings": "Ustawienia",
"@settings": {}, "@settings": {},
@@ -49,7 +49,7 @@
}, },
"biometricAuthentication": "Biometric authentication", "biometricAuthentication": "Biometric authentication",
"@biometricAuthentication": {}, "@biometricAuthentication": {},
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}", "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
"@authenticateToToggleBiometricAuthentication": { "@authenticateToToggleBiometricAuthentication": {
"placeholders": { "placeholders": {
"mode": {} "mode": {}
@@ -67,7 +67,7 @@
"@startTyping": {}, "@startTyping": {},
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?", "doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
"@doYouReallyWantToDeleteThisView": {}, "@doYouReallyWantToDeleteThisView": {},
"deleteView": "Delete view ", "deleteView": "Usuń widok ",
"@deleteView": {}, "@deleteView": {},
"addedAt": "Added at", "addedAt": "Added at",
"@addedAt": {}, "@addedAt": {},
@@ -141,13 +141,13 @@
}, },
"documentSuccessfullyDownloaded": "Document successfully downloaded.", "documentSuccessfullyDownloaded": "Document successfully downloaded.",
"@documentSuccessfullyDownloaded": {}, "@documentSuccessfullyDownloaded": {},
"suggestions": "Suggestions: ", "suggestions": "Sugestie: ",
"@suggestions": {}, "@suggestions": {},
"editDocument": "Edytuj Dokument", "editDocument": "Edytuj Dokument",
"@editDocument": {}, "@editDocument": {},
"advanced": "Advanced", "advanced": "Zaawansowane",
"@advanced": {}, "@advanced": {},
"apply": "Apply", "apply": "Zastosuj",
"@apply": {}, "@apply": {},
"extended": "Extended", "extended": "Extended",
"@extended": {}, "@extended": {},
@@ -155,7 +155,7 @@
"@titleAndContent": {}, "@titleAndContent": {},
"title": "Tytuł", "title": "Tytuł",
"@title": {}, "@title": {},
"reset": "Reset", "reset": "Zresetuj",
"@reset": {}, "@reset": {},
"filterDocuments": "Filter Documents", "filterDocuments": "Filter Documents",
"@filterDocuments": { "@filterDocuments": {
@@ -163,7 +163,7 @@
}, },
"originalMD5Checksum": "Original MD5-Checksum", "originalMD5Checksum": "Original MD5-Checksum",
"@originalMD5Checksum": {}, "@originalMD5Checksum": {},
"mediaFilename": "Media Filename", "mediaFilename": "Nazwa pliku",
"@mediaFilename": {}, "@mediaFilename": {},
"originalFileSize": "Original File Size", "originalFileSize": "Original File Size",
"@originalFileSize": {}, "@originalFileSize": {},
@@ -183,7 +183,7 @@
"@or": { "@or": {
"description": "Used on the scanner page between both main actions when no scans have been captured." "description": "Used on the scanner page between both main actions when no scans have been captured."
}, },
"deleteAllScans": "Delete all scans", "deleteAllScans": "Usuń wszystkie skany",
"@deleteAllScans": {}, "@deleteAllScans": {},
"uploadADocumentFromThisDevice": "Upload a document from this device", "uploadADocumentFromThisDevice": "Upload a document from this device",
"@uploadADocumentFromThisDevice": { "@uploadADocumentFromThisDevice": {
@@ -201,7 +201,7 @@
}, },
"searchDocuments": "Search documents", "searchDocuments": "Search documents",
"@searchDocuments": {}, "@searchDocuments": {},
"resetFilter": "Reset filter", "resetFilter": "Zresetuj filtr",
"@resetFilter": {}, "@resetFilter": {},
"lastMonth": "Last Month", "lastMonth": "Last Month",
"@lastMonth": {}, "@lastMonth": {},
@@ -221,7 +221,7 @@
"@oops": {}, "@oops": {},
"newDocumentAvailable": "New document available!", "newDocumentAvailable": "New document available!",
"@newDocumentAvailable": {}, "@newDocumentAvailable": {},
"orderBy": "Order By", "orderBy": "Sortuj według",
"@orderBy": {}, "@orderBy": {},
"thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?", "thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?",
"@thisActionIsIrreversibleDoYouWishToProceedAnyway": {}, "@thisActionIsIrreversibleDoYouWishToProceedAnyway": {},
@@ -252,7 +252,7 @@
"@fileName": {}, "@fileName": {},
"synchronizeTitleAndFilename": "Synchronize title and filename", "synchronizeTitleAndFilename": "Synchronize title and filename",
"@synchronizeTitleAndFilename": {}, "@synchronizeTitleAndFilename": {},
"reload": "Reload", "reload": "Odśwież",
"@reload": {}, "@reload": {},
"documentSuccessfullyUploadedProcessing": "Dokument pomyślnie przesłany, przetwarzam...", "documentSuccessfullyUploadedProcessing": "Dokument pomyślnie przesłany, przetwarzam...",
"@documentSuccessfullyUploadedProcessing": {}, "@documentSuccessfullyUploadedProcessing": {},

View File

@@ -49,7 +49,7 @@
}, },
"biometricAuthentication": "Biometric authentication", "biometricAuthentication": "Biometric authentication",
"@biometricAuthentication": {}, "@biometricAuthentication": {},
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}", "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
"@authenticateToToggleBiometricAuthentication": { "@authenticateToToggleBiometricAuthentication": {
"placeholders": { "placeholders": {
"mode": {} "mode": {}

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_color/dynamic_color.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_cache_manager/flutter_cache_manager.dart' as cm; import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
@@ -9,14 +10,13 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_standalone.dart'; import 'package:intl/intl_standalone.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart'; import 'package:paperless_mobile/constants.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/config/hive/hive_config.dart';
@@ -28,32 +28,33 @@ import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/service/dio_file_service.dart'; import 'package:paperless_mobile/core/service/dio_file_service.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_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/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/login/view/login_page.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/settings/global_app_settings.dart'; import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/user_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.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/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart'; import 'package:paperless_mobile/routes/document_details_route.dart';
import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/theme.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:dynamic_color/dynamic_color.dart';
String get defaultPreferredLocaleSubtag { String get defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first; String preferredLocale = Platform.localeName.split("_").first;
if (!S.supportedLocales if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) {
.any((locale) => locale.languageCode == preferredLocale)) {
preferredLocale = 'en'; preferredLocale = 'en';
} }
return preferredLocale; return preferredLocale;
@@ -61,23 +62,25 @@ String get defaultPreferredLocaleSubtag {
Future<void> _initHive() async { Future<void> _initHive() async {
await Hive.initFlutter(); await Hive.initFlutter();
//TODO: REMOVE!
// await getApplicationDocumentsDirectory().then((value) => value.delete(recursive: true));
registerHiveAdapters(); registerHiveAdapters();
final globalSettingsBox = await Hive.openBox<UserAccount>(HiveBoxes.userAccount);
await Hive.openBox<GlobalAppSettings>(HiveBoxes.globalSettings); await Hive.openBox<UserSettings>(HiveBoxes.userSettings);
if (!globalSettingsBox.containsKey(HiveBoxSingleValueKey.value)) { final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
await globalSettingsBox.put(
HiveBoxSingleValueKey.value, if (!globalSettingsBox.hasValue) {
GlobalAppSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag), await globalSettingsBox
); .setValue(GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag));
} }
} }
void main() async { void main() async {
await _initHive(); await _initHive();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final globalSettingsBox = Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
final globalSettings = Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings) final globalSettings = globalSettingsBox.getValue()!;
.get(HiveBoxSingleValueKey.value)!;
await findSystemLocale(); await findSystemLocale();
packageInfo = await PackageInfo.fromPlatform(); packageInfo = await PackageInfo.fromPlatform();
@@ -95,9 +98,8 @@ void main() async {
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity); final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
final localAuthService = LocalAuthenticationService(localAuthentication); final localAuthService = LocalAuthenticationService(localAuthentication);
final hiveDir = await getApplicationDocumentsDirectory();
HydratedBloc.storage = await HydratedStorage.build( HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: hiveDir, storageDirectory: await getApplicationDocumentsDirectory(),
); );
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -136,34 +138,27 @@ void main() async {
localAuthService, localAuthService,
authApi, authApi,
sessionManager, sessionManager,
labelRepository,
savedViewRepository,
statsApi,
); );
if (globalSettings.currentLoggedInUser != null) { if (globalSettings.currentLoggedInUser != null) {
await authCubit await authCubit.restoreSessionState();
.restoreSessionState();
}
if (authCubit.state.isAuthenticated) {
final auth = authCubit.state.authentication!;
sessionManager.updateSettings(
baseUrl: auth.serverUrl,
authToken: auth.token,
clientCertificate: auth.clientCertificate,
);
} }
final localNotificationService = LocalNotificationService(); final localNotificationService = LocalNotificationService();
await localNotificationService.initialize(); await localNotificationService.initialize();
//Update language header in interceptor on language change. //Update language header in interceptor on language change.
globalSettings.addListener( globalSettingsBox.listenable().addListener(() {
() => languageHeaderInterceptor.preferredLocaleSubtag = languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag;
globalSettings.preferredLocaleSubtag, });
);
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<PaperlessAuthenticationApi>.value(value: authApi), Provider<PaperlessAuthenticationApi>.value(value: authApi),
Provider<PaperlessDocumentsApi>.value(value: documentsApi), Provider<PaperlessDocumentsApi>.value(value: documentsApi),
Provider<PaperlessLabelsApi>.value(value: labelsApi), Provider<PaperlessLabelsApi>.value(value: labelsApi),
@@ -181,8 +176,7 @@ void main() async {
Provider<ConnectivityStatusService>.value( Provider<ConnectivityStatusService>.value(
value: connectivityStatusService, value: connectivityStatusService,
), ),
Provider<LocalNotificationService>.value( Provider<LocalNotificationService>.value(value: localNotificationService),
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()), Provider.value(value: DocumentChangedNotifier()),
], ],
child: MultiRepositoryProvider( child: MultiRepositoryProvider(
@@ -212,22 +206,13 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<PaperlessMobileEntrypoint> createState() => State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState();
_PaperlessMobileEntrypointState();
} }
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> { class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return GlobalSettingsBuilder(
providers: [
BlocProvider(
create: (context) => PaperlessServerInformationCubit(
context.read<PaperlessServerStatsApi>(),
),
),
],
child: GlobalSettingsBuilder(
builder: (context, settings) { builder: (context, settings) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
@@ -256,15 +241,13 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], ],
routes: { routes: {
DocumentDetailsRoute.routeName: (context) => DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(),
const DocumentDetailsRoute(),
}, },
home: const AuthenticationWrapper(), home: const AuthenticationWrapper(),
); );
}, },
); );
}, },
),
); );
} }
} }
@@ -279,25 +262,23 @@ class AuthenticationWrapper extends StatefulWidget {
class _AuthenticationWrapperState extends State<AuthenticationWrapper> { class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
FlutterNativeSplash.remove();
super.didChangeDependencies(); super.didChangeDependencies();
FlutterNativeSplash.remove();
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Temporary Fix: Can be removed if the flutter engine implements the fix itself
// Activate the highest supported refresh rate on the device // Activate the highest supported refresh rate on the device
if (Platform.isAndroid) { if (Platform.isAndroid) {
_setOptimalDisplayMode(); _setOptimalDisplayMode();
} }
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() ReceiveSharingIntent.getMediaStream().listen(ShareIntentQueue.instance.addAll);
.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() ReceiveSharingIntent.getInitialMedia().then(ShareIntentQueue.instance.addAll);
.then(ShareIntentQueue.instance.addAll);
} }
Future<void> _setOptimalDisplayMode() async { Future<void> _setOptimalDisplayMode() async {
@@ -309,8 +290,7 @@ 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 = final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active;
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);
@@ -318,36 +298,76 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<AuthenticationCubit, AuthenticationState>( return BlocBuilder<AuthenticationCubit, AuthenticationState>(
listener: (context, authState) { builder: (context, authentication) {
final bool showIntroSlider = if (authentication.isAuthenticated) {
authState.isAuthenticated && !authState.wasLoginStored; return MultiBlocProvider(
if (showIntroSlider) { // This key will cause the subtree to be remounted, which will again
// call the provider's create callback! Without this key, the blocs
// would not be recreated on account switch!
key: ValueKey(authentication.userId),
providers: [
BlocProvider(
create: (context) => TaskStatusCubit(
context.read<PaperlessTasksApi>(),
),
),
BlocProvider<PaperlessServerInformationCubit>(
create: (context) => PaperlessServerInformationCubit(
context.read<PaperlessServerStatsApi>(),
),
),
],
child: const HomePage(),
);
} else if (authentication.showBiometricAuthenticationScreen) {
return const VerifyIdentityPage();
}
return LoginPage(
submitText: S.of(context)!.signIn,
onSubmit: _onLogin,
);
},
);
}
void _onLogin(
BuildContext context,
String username,
String password,
String serverUrl,
ClientCertificate? clientCertificate,
) async {
try {
await context.read<AuthenticationCubit>().login(
credentials: LoginFormCredentials(username: username, password: password),
serverUrl: serverUrl,
clientCertificate: clientCertificate,
);
// Show onboarding after first login!
final globalSettings = GlobalSettings.boxedValue;
if (globalSettings.showOnboarding) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(), builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true, fullscreenDialog: true,
), ),
); ).then((value) {
globalSettings.showOnboarding = false;
globalSettings.save();
});
} }
}, } on PaperlessServerException catch (error, stackTrace) {
builder: (context, authentication) { showErrorMessage(context, error, stackTrace);
if (authentication.isAuthenticated && } on PaperlessValidationErrors catch (error, stackTrace) {
(authentication.wasLocalAuthenticationSuccessful ?? true)) { if (error.hasFieldUnspecificError) {
return BlocProvider( showLocalizedError(context, error.fieldUnspecificError!);
create: (context) =>
TaskStatusCubit(context.read<PaperlessTasksApi>()),
child: const HomePage(),
);
} else { } else {
if (authentication.wasLoginStored && showGenericError(context, error.values.first, stackTrace);
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return const VerifyIdentityPage();
} }
return const LoginPage(); } catch (unknownError, stackTrace) {
showGenericError(context, unknownError.toString(), stackTrace);
} }
},
);
} }
} }

View File

@@ -0,0 +1,14 @@
import 'package:json_annotation/json_annotation.dart';
part 'paperless_ui_settings_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class PaperlessUiSettingsModel {
final String displayName;
PaperlessUiSettingsModel({required this.displayName});
factory PaperlessUiSettingsModel.fromJson(Map<String, dynamic> json) =>
_$PaperlessUiSettingsModelFromJson(json);
Map<String, dynamic> toJson() => _$PaperlessUiSettingsModelToJson(this);
}

View File

@@ -1,7 +1,9 @@
import 'package:paperless_api/src/models/paperless_server_information_model.dart'; import 'package:paperless_api/src/models/paperless_server_information_model.dart';
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart'; import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
abstract class PaperlessServerStatsApi { abstract class PaperlessServerStatsApi {
Future<PaperlessServerInformationModel> getServerInformation(); Future<PaperlessServerInformationModel> getServerInformation();
Future<PaperlessServerStatisticsModel> getServerStatistics(); Future<PaperlessServerStatisticsModel> getServerStatistics();
Future<PaperlessUiSettingsModel> getUiSettings();
} }

View File

@@ -5,6 +5,7 @@ import 'package:http/http.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/models/paperless_server_information_model.dart'; import 'package:paperless_api/src/models/paperless_server_information_model.dart';
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart'; import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
import 'paperless_server_stats_api.dart'; import 'paperless_server_stats_api.dart';
@@ -21,15 +22,12 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override @override
Future<PaperlessServerInformationModel> getServerInformation() async { Future<PaperlessServerInformationModel> getServerInformation() async {
final response = await client.get("/api/ui_settings/"); final response = await client.get("/api/ui_settings/");
final version = response final version =
.headers[PaperlessServerInformationModel.versionHeader]?.first ?? response.headers[PaperlessServerInformationModel.versionHeader]?.first ?? 'unknown';
'unknown'; final apiVersion = int.tryParse(
final apiVersion = int.tryParse(response response.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ?? '1');
.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ??
'1');
final String username = response.data['username']; final String username = response.data['username'];
final String host = response final String host = response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
.headers[PaperlessServerInformationModel.hostHeader]?.first ??
response.headers[PaperlessServerInformationModel.hostHeader]?.first ?? response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}'); ('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}');
return PaperlessServerInformationModel( return PaperlessServerInformationModel(
@@ -48,4 +46,13 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
} }
throw const PaperlessServerException.unknown(); throw const PaperlessServerException.unknown();
} }
@override
Future<PaperlessUiSettingsModel> getUiSettings() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
return PaperlessUiSettingsModel.fromJson(response.data);
}
throw const PaperlessServerException.unknown();
}
} }