mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 05:15:51 -06:00
feat: Implement switching between accounts (multi user support), still WIP
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
19
lib/features/login/model/client_certificate_form_model.dart
Normal file
19
lib/features/login/model/client_certificate_form_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
13
lib/features/login/model/login_form_credentials.dart
Normal file
13
lib/features/login/model/login_form_credentials.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/features/login/model/user_account.dart
Normal file
20
lib/features/login/model/user_account.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
18
lib/features/login/model/user_credentials.dart
Normal file
18
lib/features/login/model/user_credentials.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()!;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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(" ")),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
136
lib/features/settings/view/manage_accounts_page.dart
Normal file
136
lib/features/settings/view/manage_accounts_page.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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..."),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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": {}
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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": {}
|
||||
|
||||
258
lib/main.dart
258
lib/main.dart
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user