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/features/login/model/authentication_information.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/user_app_settings.dart';
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
class HiveBoxes {
HiveBoxes._();
static const globalSettings = 'globalSettings';
static const userSettings = 'userSettings';
static const authentication = 'authentication';
static const vault = 'vault';
static const userCredentials = 'userCredentials';
static const userAccount = 'userAccount';
}
class HiveTypeIds {
@@ -22,18 +25,26 @@ class HiveTypeIds {
static const colorSchemeOption = 3;
static const authentication = 4;
static const clientCertificate = 5;
}
class HiveBoxSingleValueKey {
HiveBoxSingleValueKey._();
static const value = 'value';
static const userCredentials = 6;
static const userAccount = 7;
}
void registerHiveAdapters() {
Hive.registerAdapter(ColorSchemeOptionAdapter());
Hive.registerAdapter(ThemeModeAdapter());
Hive.registerAdapter(GlobalAppSettingsAdapter());
Hive.registerAdapter(UserAppSettingsAdapter());
Hive.registerAdapter(GlobalSettingsAdapter());
Hive.registerAdapter(AuthenticationInformationAdapter());
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 {
final created = await _api.saveTag(object);
final updatedState = {...state.tags}
..putIfAbsent(created.id!, () => created);
final updatedState = {...state.tags}..putIfAbsent(created.id!, () => created);
emit(state.copyWith(tags: updatedState));
return created;
}
@@ -63,8 +62,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids);
final updatedState = {...state.tags}
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
final updatedState = {...state.tags}..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(tags: updatedState));
return tags;
}
@@ -78,16 +76,14 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..putIfAbsent(created.id!, () => created);
final updatedState = {...state.correspondents}..putIfAbsent(created.id!, () => created);
emit(state.copyWith(correspondents: updatedState));
return created;
}
Future<int> deleteCorrespondent(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..removeWhere((k, v) => k == correspondent.id);
final updatedState = {...state.correspondents}..removeWhere((k, v) => k == correspondent.id);
emit(state.copyWith(correspondents: updatedState));
return correspondent.id!;
@@ -104,8 +100,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return null;
}
Future<Iterable<Correspondent>> findAllCorrespondents(
[Iterable<int>? ids]) async {
Future<Iterable<Correspondent>> findAllCorrespondents([Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids);
final updatedState = {...state.correspondents}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
@@ -116,8 +111,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..update(updated.id!, (_) => updated);
final updatedState = {...state.correspondents}..update(updated.id!, (_) => updated);
emit(state.copyWith(correspondents: updatedState));
return updated;
@@ -125,16 +119,14 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<DocumentType> createDocumentType(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.documentTypes}
..putIfAbsent(created.id!, () => created);
final updatedState = {...state.documentTypes}..putIfAbsent(created.id!, () => created);
emit(state.copyWith(documentTypes: updatedState));
return created;
}
Future<int> deleteDocumentType(DocumentType documentType) async {
await _api.deleteDocumentType(documentType);
final updatedState = {...state.documentTypes}
..removeWhere((k, v) => k == documentType.id);
final updatedState = {...state.documentTypes}..removeWhere((k, v) => k == documentType.id);
emit(state.copyWith(documentTypes: updatedState));
return documentType.id!;
}
@@ -149,8 +141,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return null;
}
Future<Iterable<DocumentType>> findAllDocumentTypes(
[Iterable<int>? ids]) async {
Future<Iterable<DocumentType>> findAllDocumentTypes([Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.documentTypes}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
@@ -160,24 +151,21 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.documentTypes}
..update(updated.id!, (_) => updated);
final updatedState = {...state.documentTypes}..update(updated.id!, (_) => updated);
emit(state.copyWith(documentTypes: updatedState));
return updated;
}
Future<StoragePath> createStoragePath(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..putIfAbsent(created.id!, () => created);
final updatedState = {...state.storagePaths}..putIfAbsent(created.id!, () => created);
emit(state.copyWith(storagePaths: updatedState));
return created;
}
Future<int> deleteStoragePath(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..removeWhere((k, v) => k == storagePath.id);
final updatedState = {...state.storagePaths}..removeWhere((k, v) => k == storagePath.id);
emit(state.copyWith(storagePaths: updatedState));
return storagePath.id!;
}
@@ -192,8 +180,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return null;
}
Future<Iterable<StoragePath>> findAllStoragePaths(
[Iterable<int>? ids]) async {
Future<Iterable<StoragePath>> findAllStoragePaths([Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.storagePaths}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
@@ -203,8 +190,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..update(updated.id!, (_) => updated);
final updatedState = {...state.storagePaths}..update(updated.id!, (_) => updated);
emit(state.copyWith(storagePaths: updatedState));
return updated;
}
@@ -217,6 +203,12 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
return super.close();
}
@override
Future<void> clear() async {
await super.clear();
emit(const LabelRepositoryState());
}
@override
LabelRepositoryState? fromJson(Map<String, dynamic> json) {
return LabelRepositoryState.fromJson(json);

View File

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

View File

@@ -32,8 +32,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
@override
Future<bool> isConnectedToInternet() async {
return _hasActiveInternetConnection(
await (Connectivity().checkConnectivity()));
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
}
@override
@@ -72,11 +71,10 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
return ReachabilityStatus.unknown;
}
try {
SessionManager manager =
SessionManager([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = const Duration(seconds: 5)
..client.options.receiveTimeout = const Duration(seconds: 5);
SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = const Duration(seconds: 5)
..client.options.receiveTimeout = const Duration(seconds: 5);
final response = await manager.client.get('$serverAddress/api/');
if (response.statusCode == 200) {
@@ -84,8 +82,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
}
return ReachabilityStatus.notReachable;
} on DioError catch (error) {
if (error.type == DioErrorType.unknown &&
error.error is ReachabilityStatus) {
if (error.type == DioErrorType.unknown && error.error is ReachabilityStatus) {
return error.error as ReachabilityStatus;
}
} 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/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
import 'package:web_socket_channel/io.dart';
abstract class StatusService {
Future<void> startListeningBeforeDocumentUpload(String httpUrl,
AuthenticationInformation credentials, String documentFileName);
Future<void> startListeningBeforeDocumentUpload(
String httpUrl, UserCredentials credentials, String documentFileName);
}
class WebSocketStatusService implements StatusService {
@@ -25,7 +26,7 @@ class WebSocketStatusService implements StatusService {
@override
Future<void> startListeningBeforeDocumentUpload(
String httpUrl,
AuthenticationInformation credentials,
UserCredentials credentials,
String documentFileName,
) async {
// socket = await WebSocket.connect(
@@ -57,7 +58,7 @@ class LongPollingStatusService implements StatusService {
@override
Future<void> startListeningBeforeDocumentUpload(
String httpUrl,
AuthenticationInformation credentials,
UserCredentials credentials,
String documentFileName,
) async {
// 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_svg/flutter_svg.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/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/generated/l10n/app_localizations.dart';
@@ -42,8 +42,7 @@ class AppDrawer extends StatelessWidget {
leading: const Icon(Icons.bug_report_outlined),
title: Text(S.of(context)!.reportABug),
onTap: () {
launchUrlString(
'https://github.com/astubenbord/paperless-mobile/issues/new');
launchUrlString('https://github.com/astubenbord/paperless-mobile/issues/new');
},
),
ListTile(
@@ -69,7 +68,10 @@ class AppDrawer extends StatelessWidget {
),
onTap: () => Navigator.of(context).push(
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});
@override
State<ApplicationIntroSlideshow> createState() =>
_ApplicationIntroSlideshowState();
State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState();
}
//TODO: INTL ALL
@@ -28,7 +27,9 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
showDoneButton: true,
next: Text(S.of(context)!.next),
done: Text(S.of(context)!.done),
onDone: () => Navigator.pop(context),
onDone: () {
Navigator.pop(context);
},
dotsDecorator: DotsDecorator(
color: Theme.of(context).colorScheme.onBackground,
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/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/settings/global_app_settings.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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -44,9 +43,8 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
width: 16,
)
: const Icon(Icons.download),
onPressed: widget.document != null && widget.enabled
? () => _onDownload(widget.document!)
: null,
onPressed:
widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null,
).paddedOnly(right: 4);
}
@@ -70,7 +68,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
setState(() => _isDownloadPending = true);
await context.read<DocumentDetailsCubit>().downloadDocument(
downloadOriginal: downloadOriginal,
locale: context.read<GlobalAppSettings>().preferredLocaleSubtag,
locale: context.read<GlobalSettings>().preferredLocaleSubtag,
);
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
} 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_state.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
as s;
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/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';
class SliverSearchBar extends StatelessWidget {
@@ -37,8 +37,7 @@ class SliverSearchBar extends StatelessWidget {
onPressed: Scaffold.of(context).openDrawer,
),
trailingIcon: IconButton(
icon: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
icon: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
builder: (context, state) {
return CircleAvatar(
child: Text(state.information?.userInitials ?? ''),
@@ -48,7 +47,10 @@ class SliverSearchBar extends StatelessWidget {
onPressed: () {
showDialog(
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 -
kBottomNavigationBarHeight -
kToolbarHeight;
final maxHeight = highlights != null
? min(600.0, availableHeight)
: min(500.0, availableHeight);
final maxHeight =
highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight);
return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
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/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
@@ -185,6 +186,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final userId = context.watch<AuthenticationCubit>().state.userId;
final destinations = [
RouteDescription(
icon: const Icon(Icons.description_outlined),
@@ -232,19 +234,20 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
];
final routes = <Widget>[
MultiBlocProvider(
key: ValueKey(userId),
providers: [
BlocProvider(
create: (context) => DocumentsCubit(
context.read(),
context.read(),
context.read(),
),
)..reload(),
),
BlocProvider(
create: (context) => SavedViewCubit(
context.read(),
context.read(),
),
)..reload(),
),
],
child: const DocumentsPage(),
@@ -254,6 +257,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
child: const ScannerPage(),
),
MultiBlocProvider(
key: ValueKey(userId),
providers: [
BlocProvider(
create: (context) => LabelCubit(context.read()),
@@ -266,12 +270,12 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
child: const InboxPage(),
),
];
return MultiBlocListener(
listeners: [
BlocListener<ConnectivityCubit, ConnectivityState>(
//Only re-initialize data if the connectivity changed from not connected to connected
listenWhen: (previous, current) =>
current == ConnectivityState.connected,
listenWhen: (previous, current) => current == ConnectivityState.connected,
listener: (context, state) {
_initializeData(context);
},
@@ -280,9 +284,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
listener: (context, state) {
if (state.task != null) {
// Handle local notifications on task change (only when app is running for now).
context
.read<LocalNotificationService>()
.notifyTaskChanged(state.task!);
context.read<LocalNotificationService>().notifyTaskChanged(state.task!);
}
},
),
@@ -295,9 +297,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: destinations
.map((e) => e.toNavigationRailDestination())
.toList(),
destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(),
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
),
@@ -315,8 +315,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
elevation: 4.0,
selectedIndex: _currentIndex,
onDestinationSelected: _onNavigationChanged,
destinations:
destinations.map((e) => e.toNavigationDestination()).toList(),
destinations: destinations.map((e) => e.toNavigationDestination()).toList(),
),
body: routes[_currentIndex],
);

View File

@@ -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/extensions/flutter_extensions.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/generated/l10n/app_localizations.dart';
@@ -33,9 +32,7 @@ class VerifyIdentityPage extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16),
const Icon(
Icons.fingerprint,
@@ -57,9 +54,7 @@ class VerifyIdentityPage extends StatelessWidget {
),
),
ElevatedButton(
onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(),
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
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_state.dart';
class InboxCubit extends HydratedCubit<InboxState>
with DocumentPagingBlocMixin {
class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin {
final LabelRepository _labelRepository;
final PaperlessDocumentsApi _documentsApi;
@@ -37,10 +36,7 @@ class InboxCubit extends HydratedCubit<InboxState>
this,
onDeleted: remove,
onUpdated: (document) {
if (document.tags
.toSet()
.intersection(state.inboxTags.toSet())
.isEmpty) {
if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) {
remove(document);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
} else {
@@ -76,28 +72,32 @@ class InboxCubit extends HydratedCubit<InboxState>
/// Fetches inbox tag ids and loads the inbox items (documents).
///
Future<void> loadInbox() async {
debugPrint("Initializing inbox...");
final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
);
if (!isClosed) {
debugPrint("Initializing inbox...");
if (inboxTags.isEmpty) {
// no inbox tags = no inbox items.
return emit(
state.copyWith(
hasLoaded: true,
value: [],
inboxTags: [],
final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
);
if (inboxTags.isEmpty) {
// no inbox tags = no inbox items.
return emit(
state.copyWith(
hasLoaded: true,
value: [],
inboxTags: [],
),
);
}
emit(state.copyWith(inboxTags: inboxTags));
updateFilter(
filter: DocumentFilter(
sortField: SortField.added,
tags: IdsTagsQuery.fromIds(inboxTags),
),
);
}
emit(state.copyWith(inboxTags: inboxTags));
updateFilter(
filter: DocumentFilter(
sortField: SortField.added,
tags: IdsTagsQuery.fromIds(inboxTags),
),
);
}
///
@@ -133,8 +133,7 @@ class InboxCubit extends HydratedCubit<InboxState>
/// from the inbox.
///
Future<Iterable<int>> removeFromInbox(DocumentModel document) async {
final tagsToRemove =
document.tags.toSet().intersection(state.inboxTags.toSet());
final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet());
final updatedTags = {...document.tags}..removeAll(tagsToRemove);
final updatedDocument = await api.update(
@@ -188,8 +187,8 @@ class InboxCubit extends HydratedCubit<InboxState>
Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) {
final int asn = await _documentsApi.findNextAsn();
final updatedDocument = await _documentsApi
.update(document.copyWith(archiveSerialNumber: () => asn));
final updatedDocument =
await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn));
replace(updatedDocument);
}

View File

@@ -1,18 +1,23 @@
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:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.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/features/login/model/authentication_information.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/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
part 'authentication_state.dart';
@@ -20,15 +25,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi;
final SessionManager _dioWrapper;
final LabelRepository _labelRepository;
final SavedViewRepository _savedViewRepository;
final PaperlessServerStatsApi _serverStatsApi;
AuthenticationCubit(
this._localAuthService,
this._authApi,
this._dioWrapper,
) : super(AuthenticationState.initial);
this._labelRepository,
this._savedViewRepository,
this._serverStatsApi,
) : super(const AuthenticationState());
Future<void> login({
required UserCredentials credentials,
required LoginFormCredentials credentials,
required String serverUrl,
ClientCertificate? clientCertificate,
}) async {
@@ -47,107 +58,239 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCertificate,
authToken: token,
);
final authInfo = AuthenticationInformation(
username: credentials.username!,
serverUrl: serverUrl,
clientCertificate: clientCertificate,
token: token,
);
final userId = "${credentials.username}@$serverUrl";
// If it is first time login, create settings for this user.
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
if (!userSettingsBox.containsKey(userId)) {
userSettingsBox.put(userId, UserSettings());
}
final fullName = await _fetchFullName();
if (!userAccountBox.containsKey(userId)) {
userAccountBox.put(
userId,
UserAccount(
serverUrl: serverUrl,
username: credentials.username!,
fullName: fullName,
),
);
}
// Mark logged in user as currently active user.
final globalSettings = GlobalAppSettings.boxedValue;
final globalSettings = GlobalSettings.boxedValue;
globalSettings.currentLoggedInUser = userId;
await globalSettings.save();
globalSettings.save();
// Save credentials in encrypted box
final encryptedBox = await _openEncryptedBox();
await encryptedBox.put(
final userCredentialsBox = await _getUserCredentialsBox();
await userCredentialsBox.put(
userId,
authInfo,
);
encryptedBox.close();
emit(
AuthenticationState(
wasLoginStored: false,
authentication: authInfo,
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.
///
Future<void> restoreSessionState() async {
final globalSettings = GlobalAppSettings.boxedValue;
if (globalSettings.currentLoggedInUser == null) {
final globalSettings = GlobalSettings.boxedValue;
final userId = globalSettings.currentLoggedInUser;
if (userId == null) {
// If there is nothing to restore, we can quit here.
return;
}
final userSettings = Hive.box<UserAppSettings>(HiveBoxes.userSettings)
.get(globalSettings.currentLoggedInUser!);
final userSettings = Hive.box<UserSettings>(HiveBoxes.userSettings).get(userId)!;
final userAccount = Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!;
if (userSettings!.isBiometricAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (localAuthSuccess) {
final authentication = await _readAuthenticationFromEncryptedBox(
globalSettings.currentLoggedInUser!);
if (authentication != null) {
_dioWrapper.updateSettings(
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: authentication.serverUrl,
);
return emit(
AuthenticationState(
wasLoginStored: true,
authentication: state.authentication,
wasLocalAuthenticationSuccessful: true,
),
);
}
} else {
return emit(
AuthenticationState(
wasLoginStored: true,
wasLocalAuthenticationSuccessful: false,
authentication: null,
),
);
if (userSettings.isBiometricAuthenticationEnabled) {
final localAuthSuccess =
await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (!localAuthSuccess) {
emit(const AuthenticationState(showBiometricAuthenticationScreen: true));
return;
}
}
final userCredentialsBox = await _getUserCredentialsBox();
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!);
if (authentication != null) {
_dioWrapper.updateSettings(
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: userAccount.serverUrl,
serverInformation: PaperlessServerInformationModel(),
);
emit(
AuthenticationState(
isAuthenticated: true,
showBiometricAuthenticationScreen: false,
username: userAccount.username,
),
);
} else {
final authentication = await _readAuthenticationFromEncryptedBox(
globalSettings.currentLoggedInUser!);
if (authentication != null) {
_dioWrapper.updateSettings(
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: authentication.serverUrl,
);
emit(
AuthenticationState(
authentication: authentication,
wasLoginStored: true,
),
);
} else {
return emit(AuthenticationState.initial);
}
throw Exception("User should be authenticated but no authentication information was found.");
}
}
Future<AuthenticationInformation?> _readAuthenticationFromEncryptedBox(
String userId) {
return _openEncryptedBox().then((box) => box.get(userId));
Future<void> logout() async {
await _resetExternalState();
final globalSettings = GlobalSettings.boxedValue;
globalSettings
..currentLoggedInUser = null
..save();
emit(const AuthenticationState());
}
Future<Box<AuthenticationInformation?>> _openEncryptedBox() async {
Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage();
final encryptionKeyString = await secureStorage.read(key: 'key');
if (encryptionKeyString == null) {
if (!await secureStorage.containsKey(key: 'key')) {
final key = Hive.generateSecureKey();
await secureStorage.write(
@@ -155,17 +298,40 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
value: base64UrlEncode(key),
);
}
final key = await secureStorage.read(key: 'key');
final encryptionKeyUint8List = base64Url.decode(key!);
return await Hive.openBox<AuthenticationInformation>(
HiveBoxes.vault,
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
final key = (await secureStorage.read(key: 'key'))!;
return base64Decode(key);
}
Future<Box<UserCredentials>> _getUserCredentialsBox() async {
final keyBytes = await _getEncryptedBoxKey();
return Hive.openBox<UserCredentials>(
HiveBoxes.userCredentials,
encryptionCipher: HiveAesCipher(keyBytes),
);
}
Future<void> logout() async {
await Hive.box<AuthenticationInformation>(HiveBoxes.authentication).clear();
Future<void> _resetExternalState() {
_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';
@JsonSerializable()
class AuthenticationState {
final bool wasLoginStored;
@JsonKey(includeFromJson: false, includeToJson: false)
final bool? wasLocalAuthenticationSuccessful;
final AuthenticationInformation? authentication;
class AuthenticationState with EquatableMixin {
final bool showBiometricAuthenticationScreen;
final bool isAuthenticated;
final String? username;
final String? fullName;
final String? userId;
static final AuthenticationState initial = AuthenticationState(
wasLoginStored: false,
);
bool get isAuthenticated => authentication != null;
AuthenticationState({
required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
this.authentication,
const AuthenticationState({
this.isAuthenticated = false,
this.showBiometricAuthenticationScreen = false,
this.username,
this.fullName,
this.userId,
});
AuthenticationState copyWith({
bool? wasLoginStored,
bool? isAuthenticated,
AuthenticationInformation? authentication,
bool? wasLocalAuthenticationSuccessful,
bool? showBiometricAuthenticationScreen,
String? username,
String? fullName,
String? userId,
}) {
return AuthenticationState(
wasLoginStored: wasLoginStored ?? this.wasLoginStored,
authentication: authentication ?? this.authentication,
wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ??
this.wasLocalAuthenticationSuccessful,
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
showBiometricAuthenticationScreen:
showBiometricAuthenticationScreen ?? this.showBiometricAuthenticationScreen,
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:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/type/types.dart';
part 'client_certificate.g.dart';
@HiveType(typeId: HiveTypeIds.clientCertificate)
class ClientCertificate {
static const bytesKey = 'bytes';
static const passphraseKey = 'passphrase';
@HiveField(0)
final Uint8List bytes;
Uint8List bytes;
@HiveField(1)
final String? passphrase;
String? 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:paperless_api/paperless_api.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/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/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/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 'widgets/login_pages/server_login_page.dart';
import 'widgets/never_scrollable_scroll_behavior.dart';
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
State<LoginPage> createState() => _LoginPageState();
@@ -46,7 +65,8 @@ class _LoginPageState extends State<LoginPage> {
),
ServerLoginPage(
formBuilderKey: _formKey,
onDone: _login,
submitText: widget.submitText,
onSubmit: _login,
),
],
),
@@ -58,24 +78,23 @@ class _LoginPageState extends State<LoginPage> {
FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value;
try {
await context.read<AuthenticationCubit>().login(
credentials: form[UserCredentialsFormField.fkCredentials],
serverUrl: form[ServerAddressFormField.fkServerAddress],
clientCertificate:
form[ClientCertificateFormField.fkClientCertificate],
);
} 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);
ClientCertificate? clientCert;
final clientCertFormModel =
form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?;
if (clientCertFormModel != null) {
clientCert = ClientCertificate(
bytes: clientCertFormModel.bytes,
passphrase: clientCertFormModel.passphrase,
);
}
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: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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/constants.dart';
@@ -15,23 +16,21 @@ import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget {
static const fkClientCertificate = 'clientCertificate';
final void Function(ClientCertificate? cert) onChanged;
final void Function(ClientCertificateFormModel? cert) onChanged;
const ClientCertificateFormField({
Key? key,
required this.onChanged,
}) : super(key: key);
@override
State<ClientCertificateFormField> createState() =>
_ClientCertificateFormFieldState();
State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState();
}
class _ClientCertificateFormFieldState
extends State<ClientCertificateFormField> {
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> {
File? _selectedFile;
@override
Widget build(BuildContext context) {
return FormBuilderField<ClientCertificate?>(
return FormBuilderField<ClientCertificateFormModel?>(
key: const ValueKey('login-client-cert'),
onChanged: widget.onChanged,
initialValue: null,
@@ -46,8 +45,7 @@ class _ClientCertificateFormFieldState
return null;
},
builder: (field) {
final theme =
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
return Theme(
data: theme,
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(
allowMultiple: false,
);
@@ -133,14 +131,13 @@ class _ClientCertificateFormFieldState
setState(() {
_selectedFile = file;
});
final changedValue =
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
ClientCertificate(bytes: file.readAsBytesSync());
final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ??
ClientCertificateFormModel(bytes: file.readAsBytesSync());
field.didChange(changedValue);
}
}
Widget _buildSelectedFileText(FormFieldState<ClientCertificate?> field) {
Widget _buildSelectedFileText(FormFieldState<ClientCertificateFormModel?> field) {
if (field.value == null) {
assert(_selectedFile == null);
return Text(

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/generated/l10n/app_localizations.dart';
@@ -14,14 +14,13 @@ class UserCredentialsFormField extends StatefulWidget {
}) : super(key: key);
@override
State<UserCredentialsFormField> createState() =>
_UserCredentialsFormFieldState();
State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState();
}
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
@override
Widget build(BuildContext context) {
return FormBuilderField<UserCredentials?>(
return FormBuilderField<LoginFormCredentials?>(
name: UserCredentialsFormField.fkCredentials,
builder: (field) => AutofillGroup(
child: Column(
@@ -34,7 +33,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
autocorrect: false,
onChanged: (username) => field.didChange(
field.value?.copyWith(username: username) ??
UserCredentials(username: username),
LoginFormCredentials(username: username),
),
validator: (value) {
if (value?.trim().isEmpty ?? true) {
@@ -51,7 +50,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
label: S.of(context)!.password,
onChanged: (password) => field.didChange(
field.value?.copyWith(password: password) ??
UserCredentials(password: password),
LoginFormCredentials(password: password),
),
validator: (value) {
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/widgets/paperless_logo.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/view/widgets/form_fields/client_certificate_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,
title: Text(S.of(context)!.connectToPaperless),
bottom: PreferredSize(
child: _isCheckingConnection
? const LinearProgressIndicator()
: const SizedBox(height: 4.0),
child:
_isCheckingConnection ? const LinearProgressIndicator() : const SizedBox(height: 4.0),
preferredSize: const Size.fromHeight(4.0),
),
),
@@ -67,9 +68,8 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
),
FilledButton(
child: Text(S.of(context)!.continueLabel),
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
? widget.onContinue
: null,
onPressed:
_reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null,
),
],
),
@@ -81,16 +81,16 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
setState(() {
_isCheckingConnection = true;
});
final status = await context
.read<ConnectivityStatusService>()
.isPaperlessServerReachable(
final certForm = widget.formBuilderKey.currentState
?.getRawValue(ClientCertificateFormField.fkClientCertificate)
as ClientCertificateFormModel?;
final status = await context.read<ConnectivityStatusService>().isPaperlessServerReachable(
address ??
widget.formBuilderKey.currentState!
.getRawValue(ServerAddressFormField.fkServerAddress),
widget.formBuilderKey.currentState?.getRawValue(
ClientCertificateFormField.fkClientCertificate,
),
certForm != null
? ClientCertificate(bytes: certForm.bytes, passphrase: certForm.passphrase)
: null,
);
setState(() {
_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';
class ServerLoginPage extends StatefulWidget {
final Future<void> Function() onDone;
final String submitText;
final Future<void> Function() onSubmit;
final GlobalKey<FormBuilderState> formBuilderKey;
const ServerLoginPage({
super.key,
required this.onDone,
required this.onSubmit,
required this.formBuilderKey,
required this.submitText,
});
@override
@@ -23,8 +25,7 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
@override
Widget build(BuildContext context) {
final serverAddress = (widget.formBuilderKey.currentState
?.getRawValue(ServerAddressFormField.fkServerAddress)
as String?)
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
?.replaceAll(RegExp(r'https?://'), '') ??
'';
return Scaffold(
@@ -50,7 +51,7 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
FilledButton(
onPressed: () async {
setState(() => _isLoginLoading = true);
await widget.onDone();
await widget.onSubmit();
setState(() => _isLoginLoading = false);
},
child: Text(S.of(context)!.signIn),

View File

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

View File

@@ -2,10 +2,10 @@ import 'package:flutter/material.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_state.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
as s;
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
import 'package:paperless_mobile/extensions/flutter_extensions.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);
@@ -47,8 +47,7 @@ class _SearchAppBarState extends State<SearchAppBar> {
onPressed: Scaffold.of(context).openDrawer,
),
trailingIcon: IconButton(
icon: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
icon: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
builder: (context, state) {
return CircleAvatar(
child: Text(state.information?.userInitials ?? ''),
@@ -58,7 +57,10 @@ class _SearchAppBarState extends State<SearchAppBar> {
onPressed: () {
showDialog(
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/features/settings/model/color_scheme_option.dart';
part 'global_app_settings.g.dart';
part 'global_settings.g.dart';
@HiveType(typeId: HiveTypeIds.globalSettings)
class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
class GlobalSettings with HiveObjectMixin {
@HiveField(0)
String preferredLocaleSubtag;
@@ -22,7 +22,7 @@ class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
@HiveField(4)
String? currentLoggedInUser;
GlobalAppSettings({
GlobalSettings({
required this.preferredLocaleSubtag,
this.preferredThemeMode = ThemeMode.system,
this.preferredColorSchemeOption = ColorSchemeOption.classic,
@@ -30,7 +30,6 @@ class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
this.currentLoggedInUser,
});
static GlobalAppSettings get boxedValue =>
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
.get(HiveBoxSingleValueKey.value)!;
static GlobalSettings get boxedValue =>
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
}

View File

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

View File

@@ -1,18 +1,18 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.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_state.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/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/login/model/user_account.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/helpers/message_helpers.dart';
class AccountSettingsDialog extends StatelessWidget {
@@ -20,74 +20,95 @@ class AccountSettingsDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
scrollable: true,
contentPadding: EdgeInsets.zero,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.account),
const CloseButton(),
],
),
content: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
builder: (context, state) {
return Column(
children: [
ExpansionTile(
leading: CircleAvatar(
child: Text(state.information?.userInitials ?? ''),
return GlobalSettingsBuilder(builder: (context, globalSettings) {
return AlertDialog(
insetPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 32),
scrollable: true,
contentPadding: EdgeInsets.zero,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.account),
const CloseButton(),
],
),
content: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
builder: (context, state) {
return Column(
children: [
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(
child: Text(state.information?.userInitials ?? ''),
),
title: Text(state.information?.username ?? ''),
subtitle: Text(state.information?.host ?? ''),
children:
accounts.map((account) => _buildAccountTile(account, true)).toList(),
);
},
),
title: Text(state.information?.username ?? ''),
subtitle: Text(state.information?.host ?? ''),
children: const [
HintCard(
hintText: "WIP: Coming soon with multi user support!",
),
],
),
const Divider(),
ListTile(
dense: true,
leading: const Icon(Icons.person_add_rounded),
title: Text(S.of(context)!.addAnotherAccount),
onTap: () {},
),
const Divider(),
FilledButton(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.error,
),
ListTile(
dense: true,
leading: const Icon(Icons.person_add_rounded),
title: Text(S.of(context)!.addAnotherAccount),
onTap: () {},
),
child: Text(
S.of(context)!.disconnect,
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
const Divider(),
FilledButton(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
onPressed: () async {
await _onLogout(context);
Navigator.of(context).maybePop();
},
).padded(16),
],
);
},
),
);
child: Text(
S.of(context)!.disconnect,
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
),
),
onPressed: () async {
await _onLogout(context);
Navigator.of(context).maybePop();
},
).padded(16),
],
);
},
),
);
});
}
Future<void> _onLogout(BuildContext context) async {
try {
await context.read<AuthenticationCubit>().logout();
await context.read<GlobalAppSettings>();
await context.read<LabelRepository>().clear();
await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear();
} on PaperlessServerException catch (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:paperless_mobile/core/bloc/paperless_server_information_cubit.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/security_settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@@ -18,14 +15,13 @@ class SettingsPage extends StatelessWidget {
appBar: AppBar(
title: Text(S.of(context)!.settings),
),
bottomNavigationBar: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
bottomNavigationBar:
BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
builder: (context, state) {
final info = state.information!;
return ListTile(
title: Text(
S.of(context)!.loggedInAs(info.username ?? 'unknown') +
"@${info.host}",
S.of(context)!.loggedInAs(info.username ?? 'unknown') + "@${info.host}",
style: Theme.of(context).textTheme.labelSmall,
textAlign: TextAlign.center,
),

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_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/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.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});
@override
State<LanguageSelectionSetting> createState() =>
_LanguageSelectionSettingState();
State<LanguageSelectionSetting> createState() => _LanguageSelectionSettingState();
}
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {

View File

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

View File

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

View File

@@ -49,7 +49,7 @@
},
"biometricAuthentication": "Biometric authentication",
"@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": {
"placeholders": {
"mode": {}

View File

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

View File

@@ -49,7 +49,7 @@
},
"biometricAuthentication": "Biometric authentication",
"@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": {
"placeholders": {
"mode": {}

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_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_bloc/flutter_bloc.dart';
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_native_splash/flutter_native_splash.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_standalone.dart';
import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.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/paperless_server_information_cubit.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/service/connectivity_status_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/home/view/home_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/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/view/login_page.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/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.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/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/theme.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:dynamic_color/dynamic_color.dart';
String get defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first;
if (!S.supportedLocales
.any((locale) => locale.languageCode == preferredLocale)) {
if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) {
preferredLocale = 'en';
}
return preferredLocale;
@@ -61,23 +62,25 @@ String get defaultPreferredLocaleSubtag {
Future<void> _initHive() async {
await Hive.initFlutter();
//TODO: REMOVE!
// await getApplicationDocumentsDirectory().then((value) => value.delete(recursive: true));
registerHiveAdapters();
final globalSettingsBox =
await Hive.openBox<GlobalAppSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.containsKey(HiveBoxSingleValueKey.value)) {
await globalSettingsBox.put(
HiveBoxSingleValueKey.value,
GlobalAppSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag),
);
await Hive.openBox<UserAccount>(HiveBoxes.userAccount);
await Hive.openBox<UserSettings>(HiveBoxes.userSettings);
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.hasValue) {
await globalSettingsBox
.setValue(GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag));
}
}
void main() async {
await _initHive();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final globalSettings = Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
.get(HiveBoxSingleValueKey.value)!;
final globalSettingsBox = Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
final globalSettings = globalSettingsBox.getValue()!;
await findSystemLocale();
packageInfo = await PackageInfo.fromPlatform();
@@ -95,9 +98,8 @@ void main() async {
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
final localAuthService = LocalAuthenticationService(localAuthentication);
final hiveDir = await getApplicationDocumentsDirectory();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: hiveDir,
storageDirectory: await getApplicationDocumentsDirectory(),
);
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -136,34 +138,27 @@ void main() async {
localAuthService,
authApi,
sessionManager,
labelRepository,
savedViewRepository,
statsApi,
);
if (globalSettings.currentLoggedInUser != null) {
await authCubit
.restoreSessionState();
}
if (authCubit.state.isAuthenticated) {
final auth = authCubit.state.authentication!;
sessionManager.updateSettings(
baseUrl: auth.serverUrl,
authToken: auth.token,
clientCertificate: auth.clientCertificate,
);
await authCubit.restoreSessionState();
}
final localNotificationService = LocalNotificationService();
await localNotificationService.initialize();
//Update language header in interceptor on language change.
globalSettings.addListener(
() => languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag,
);
globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag;
});
runApp(
MultiProvider(
providers: [
Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<PaperlessAuthenticationApi>.value(value: authApi),
Provider<PaperlessDocumentsApi>.value(value: documentsApi),
Provider<PaperlessLabelsApi>.value(value: labelsApi),
@@ -181,8 +176,7 @@ void main() async {
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService,
),
Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider<LocalNotificationService>.value(value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()),
],
child: MultiRepositoryProvider(
@@ -212,59 +206,48 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
}) : super(key: key);
@override
State<PaperlessMobileEntrypoint> createState() =>
_PaperlessMobileEntrypointState();
State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState();
}
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => PaperlessServerInformationCubit(
context.read<PaperlessServerStatsApi>(),
),
),
],
child: GlobalSettingsBuilder(
builder: (context, settings) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: buildTheme(
brightness: Brightness.light,
dynamicScheme: lightDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
darkTheme: buildTheme(
brightness: Brightness.dark,
dynamicScheme: darkDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
themeMode: settings.preferredThemeMode,
supportedLocales: S.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
routes: {
DocumentDetailsRoute.routeName: (context) =>
const DocumentDetailsRoute(),
},
home: const AuthenticationWrapper(),
);
},
);
},
),
return GlobalSettingsBuilder(
builder: (context, settings) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: buildTheme(
brightness: Brightness.light,
dynamicScheme: lightDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
darkTheme: buildTheme(
brightness: Brightness.dark,
dynamicScheme: darkDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
themeMode: settings.preferredThemeMode,
supportedLocales: S.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
routes: {
DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(),
},
home: const AuthenticationWrapper(),
);
},
);
},
);
}
}
@@ -279,25 +262,23 @@ class AuthenticationWrapper extends StatefulWidget {
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override
void didChangeDependencies() {
FlutterNativeSplash.remove();
super.didChangeDependencies();
FlutterNativeSplash.remove();
}
@override
void 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
if (Platform.isAndroid) {
_setOptimalDisplayMode();
}
initializeDateFormatting();
// For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream()
.listen(ShareIntentQueue.instance.addAll);
ReceiveSharingIntent.getMediaStream().listen(ShareIntentQueue.instance.addAll);
// For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia()
.then(ShareIntentQueue.instance.addAll);
ReceiveSharingIntent.getInitialMedia().then(ShareIntentQueue.instance.addAll);
}
Future<void> _setOptimalDisplayMode() async {
@@ -309,8 +290,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
.toList()
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
final DisplayMode mostOptimalMode =
sameResolution.isNotEmpty ? sameResolution.first : active;
final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active;
debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
@@ -318,36 +298,76 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override
Widget build(BuildContext context) {
return BlocConsumer<AuthenticationCubit, AuthenticationState>(
listener: (context, authState) {
final bool showIntroSlider =
authState.isAuthenticated && !authState.wasLoginStored;
if (showIntroSlider) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true,
),
);
}
},
return BlocBuilder<AuthenticationCubit, AuthenticationState>(
builder: (context, authentication) {
if (authentication.isAuthenticated &&
(authentication.wasLocalAuthenticationSuccessful ?? true)) {
return BlocProvider(
create: (context) =>
TaskStatusCubit(context.read<PaperlessTasksApi>()),
if (authentication.isAuthenticated) {
return MultiBlocProvider(
// 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.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return const VerifyIdentityPage();
}
return const LoginPage();
} 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(
context,
MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true,
),
).then((value) {
globalSettings.showOnboarding = false;
globalSettings.save();
});
}
} 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);
}
}
}

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_statistics_model.dart';
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
abstract class PaperlessServerStatsApi {
Future<PaperlessServerInformationModel> getServerInformation();
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_information_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';
@@ -21,15 +22,12 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override
Future<PaperlessServerInformationModel> getServerInformation() async {
final response = await client.get("/api/ui_settings/");
final version = response
.headers[PaperlessServerInformationModel.versionHeader]?.first ??
'unknown';
final apiVersion = int.tryParse(response
.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ??
'1');
final version =
response.headers[PaperlessServerInformationModel.versionHeader]?.first ?? 'unknown';
final apiVersion = int.tryParse(
response.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ?? '1');
final String username = response.data['username'];
final String host = response
.headers[PaperlessServerInformationModel.hostHeader]?.first ??
final String host = response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}');
return PaperlessServerInformationModel(
@@ -48,4 +46,13 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
}
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();
}
}