mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 08:07:56 -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/core/config/hive/custpm_adapters/theme_mode_adapter.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||||
|
|
||||||
class HiveBoxes {
|
class HiveBoxes {
|
||||||
HiveBoxes._();
|
HiveBoxes._();
|
||||||
static const globalSettings = 'globalSettings';
|
static const globalSettings = 'globalSettings';
|
||||||
static const userSettings = 'userSettings';
|
static const userSettings = 'userSettings';
|
||||||
static const authentication = 'authentication';
|
static const authentication = 'authentication';
|
||||||
static const vault = 'vault';
|
static const userCredentials = 'userCredentials';
|
||||||
|
static const userAccount = 'userAccount';
|
||||||
}
|
}
|
||||||
|
|
||||||
class HiveTypeIds {
|
class HiveTypeIds {
|
||||||
@@ -22,18 +25,26 @@ class HiveTypeIds {
|
|||||||
static const colorSchemeOption = 3;
|
static const colorSchemeOption = 3;
|
||||||
static const authentication = 4;
|
static const authentication = 4;
|
||||||
static const clientCertificate = 5;
|
static const clientCertificate = 5;
|
||||||
}
|
static const userCredentials = 6;
|
||||||
|
static const userAccount = 7;
|
||||||
class HiveBoxSingleValueKey {
|
|
||||||
HiveBoxSingleValueKey._();
|
|
||||||
static const value = 'value';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerHiveAdapters() {
|
void registerHiveAdapters() {
|
||||||
Hive.registerAdapter(ColorSchemeOptionAdapter());
|
Hive.registerAdapter(ColorSchemeOptionAdapter());
|
||||||
Hive.registerAdapter(ThemeModeAdapter());
|
Hive.registerAdapter(ThemeModeAdapter());
|
||||||
Hive.registerAdapter(GlobalAppSettingsAdapter());
|
Hive.registerAdapter(GlobalSettingsAdapter());
|
||||||
Hive.registerAdapter(UserAppSettingsAdapter());
|
|
||||||
Hive.registerAdapter(AuthenticationInformationAdapter());
|
Hive.registerAdapter(AuthenticationInformationAdapter());
|
||||||
Hive.registerAdapter(ClientCertificateAdapter());
|
Hive.registerAdapter(ClientCertificateAdapter());
|
||||||
|
Hive.registerAdapter(UserSettingsAdapter());
|
||||||
|
Hive.registerAdapter(UserCredentialsAdapter());
|
||||||
|
Hive.registerAdapter(UserAccountAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HiveSingleValueBox<T> on Box<T> {
|
||||||
|
static const _valueKey = 'SINGLE_VALUE';
|
||||||
|
bool get hasValue => containsKey(_valueKey);
|
||||||
|
|
||||||
|
T? getValue() => get(_valueKey);
|
||||||
|
|
||||||
|
Future<void> setValue(T value) => put(_valueKey, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<Tag> createTag(Tag object) async {
|
Future<Tag> createTag(Tag object) async {
|
||||||
final created = await _api.saveTag(object);
|
final created = await _api.saveTag(object);
|
||||||
final updatedState = {...state.tags}
|
final updatedState = {...state.tags}..putIfAbsent(created.id!, () => created);
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(state.copyWith(tags: updatedState));
|
emit(state.copyWith(tags: updatedState));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
@@ -63,8 +62,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
|
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
|
||||||
final tags = await _api.getTags(ids);
|
final tags = await _api.getTags(ids);
|
||||||
final updatedState = {...state.tags}
|
final updatedState = {...state.tags}..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
||||||
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
|
||||||
emit(state.copyWith(tags: updatedState));
|
emit(state.copyWith(tags: updatedState));
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
@@ -78,16 +76,14 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
|
Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
|
||||||
final created = await _api.saveCorrespondent(correspondent);
|
final created = await _api.saveCorrespondent(correspondent);
|
||||||
final updatedState = {...state.correspondents}
|
final updatedState = {...state.correspondents}..putIfAbsent(created.id!, () => created);
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(state.copyWith(correspondents: updatedState));
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> deleteCorrespondent(Correspondent correspondent) async {
|
Future<int> deleteCorrespondent(Correspondent correspondent) async {
|
||||||
await _api.deleteCorrespondent(correspondent);
|
await _api.deleteCorrespondent(correspondent);
|
||||||
final updatedState = {...state.correspondents}
|
final updatedState = {...state.correspondents}..removeWhere((k, v) => k == correspondent.id);
|
||||||
..removeWhere((k, v) => k == correspondent.id);
|
|
||||||
emit(state.copyWith(correspondents: updatedState));
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
|
||||||
return correspondent.id!;
|
return correspondent.id!;
|
||||||
@@ -104,8 +100,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<Correspondent>> findAllCorrespondents(
|
Future<Iterable<Correspondent>> findAllCorrespondents([Iterable<int>? ids]) async {
|
||||||
[Iterable<int>? ids]) async {
|
|
||||||
final correspondents = await _api.getCorrespondents(ids);
|
final correspondents = await _api.getCorrespondents(ids);
|
||||||
final updatedState = {...state.correspondents}
|
final updatedState = {...state.correspondents}
|
||||||
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
||||||
@@ -116,8 +111,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
|
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
|
||||||
final updated = await _api.updateCorrespondent(correspondent);
|
final updated = await _api.updateCorrespondent(correspondent);
|
||||||
final updatedState = {...state.correspondents}
|
final updatedState = {...state.correspondents}..update(updated.id!, (_) => updated);
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(state.copyWith(correspondents: updatedState));
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
@@ -125,16 +119,14 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<DocumentType> createDocumentType(DocumentType documentType) async {
|
Future<DocumentType> createDocumentType(DocumentType documentType) async {
|
||||||
final created = await _api.saveDocumentType(documentType);
|
final created = await _api.saveDocumentType(documentType);
|
||||||
final updatedState = {...state.documentTypes}
|
final updatedState = {...state.documentTypes}..putIfAbsent(created.id!, () => created);
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(state.copyWith(documentTypes: updatedState));
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> deleteDocumentType(DocumentType documentType) async {
|
Future<int> deleteDocumentType(DocumentType documentType) async {
|
||||||
await _api.deleteDocumentType(documentType);
|
await _api.deleteDocumentType(documentType);
|
||||||
final updatedState = {...state.documentTypes}
|
final updatedState = {...state.documentTypes}..removeWhere((k, v) => k == documentType.id);
|
||||||
..removeWhere((k, v) => k == documentType.id);
|
|
||||||
emit(state.copyWith(documentTypes: updatedState));
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
return documentType.id!;
|
return documentType.id!;
|
||||||
}
|
}
|
||||||
@@ -149,8 +141,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<DocumentType>> findAllDocumentTypes(
|
Future<Iterable<DocumentType>> findAllDocumentTypes([Iterable<int>? ids]) async {
|
||||||
[Iterable<int>? ids]) async {
|
|
||||||
final documentTypes = await _api.getDocumentTypes(ids);
|
final documentTypes = await _api.getDocumentTypes(ids);
|
||||||
final updatedState = {...state.documentTypes}
|
final updatedState = {...state.documentTypes}
|
||||||
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
||||||
@@ -160,24 +151,21 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
|
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
|
||||||
final updated = await _api.updateDocumentType(documentType);
|
final updated = await _api.updateDocumentType(documentType);
|
||||||
final updatedState = {...state.documentTypes}
|
final updatedState = {...state.documentTypes}..update(updated.id!, (_) => updated);
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(state.copyWith(documentTypes: updatedState));
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StoragePath> createStoragePath(StoragePath storagePath) async {
|
Future<StoragePath> createStoragePath(StoragePath storagePath) async {
|
||||||
final created = await _api.saveStoragePath(storagePath);
|
final created = await _api.saveStoragePath(storagePath);
|
||||||
final updatedState = {...state.storagePaths}
|
final updatedState = {...state.storagePaths}..putIfAbsent(created.id!, () => created);
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(state.copyWith(storagePaths: updatedState));
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> deleteStoragePath(StoragePath storagePath) async {
|
Future<int> deleteStoragePath(StoragePath storagePath) async {
|
||||||
await _api.deleteStoragePath(storagePath);
|
await _api.deleteStoragePath(storagePath);
|
||||||
final updatedState = {...state.storagePaths}
|
final updatedState = {...state.storagePaths}..removeWhere((k, v) => k == storagePath.id);
|
||||||
..removeWhere((k, v) => k == storagePath.id);
|
|
||||||
emit(state.copyWith(storagePaths: updatedState));
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
return storagePath.id!;
|
return storagePath.id!;
|
||||||
}
|
}
|
||||||
@@ -192,8 +180,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<StoragePath>> findAllStoragePaths(
|
Future<Iterable<StoragePath>> findAllStoragePaths([Iterable<int>? ids]) async {
|
||||||
[Iterable<int>? ids]) async {
|
|
||||||
final storagePaths = await _api.getStoragePaths(ids);
|
final storagePaths = await _api.getStoragePaths(ids);
|
||||||
final updatedState = {...state.storagePaths}
|
final updatedState = {...state.storagePaths}
|
||||||
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
||||||
@@ -203,8 +190,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
|
|
||||||
Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
|
Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
|
||||||
final updated = await _api.updateStoragePath(storagePath);
|
final updated = await _api.updateStoragePath(storagePath);
|
||||||
final updatedState = {...state.storagePaths}
|
final updatedState = {...state.storagePaths}..update(updated.id!, (_) => updated);
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(state.copyWith(storagePaths: updatedState));
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -217,6 +203,12 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
|||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
await super.clear();
|
||||||
|
emit(const LabelRepositoryState());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
LabelRepositoryState? fromJson(Map<String, dynamic> json) {
|
LabelRepositoryState? fromJson(Map<String, dynamic> json) {
|
||||||
return LabelRepositoryState.fromJson(json);
|
return LabelRepositoryState.fromJson(json);
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
|
|||||||
|
|
||||||
Future<SavedView> create(SavedView object) async {
|
Future<SavedView> create(SavedView object) async {
|
||||||
final created = await _api.save(object);
|
final created = await _api.save(object);
|
||||||
final updatedState = {...state.savedViews}
|
final updatedState = {...state.savedViews}..putIfAbsent(created.id!, () => created);
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(state.copyWith(savedViews: updatedState));
|
emit(state.copyWith(savedViews: updatedState));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
@@ -43,8 +42,7 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
|
|||||||
Future<SavedView?> find(int id) async {
|
Future<SavedView?> find(int id) async {
|
||||||
final found = await _api.find(id);
|
final found = await _api.find(id);
|
||||||
if (found != null) {
|
if (found != null) {
|
||||||
final updatedState = {...state.savedViews}
|
final updatedState = {...state.savedViews}..update(id, (_) => found, ifAbsent: () => found);
|
||||||
..update(id, (_) => found, ifAbsent: () => found);
|
|
||||||
emit(state.copyWith(savedViews: updatedState));
|
emit(state.copyWith(savedViews: updatedState));
|
||||||
}
|
}
|
||||||
return found;
|
return found;
|
||||||
@@ -68,6 +66,12 @@ class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
|
|||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
await super.clear();
|
||||||
|
emit(const SavedViewRepositoryState());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
|
SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
|
||||||
return SavedViewRepositoryState.fromJson(json);
|
return SavedViewRepositoryState.fromJson(json);
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isConnectedToInternet() async {
|
Future<bool> isConnectedToInternet() async {
|
||||||
return _hasActiveInternetConnection(
|
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
|
||||||
await (Connectivity().checkConnectivity()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -72,8 +71,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
return ReachabilityStatus.unknown;
|
return ReachabilityStatus.unknown;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
SessionManager manager =
|
SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()])
|
||||||
SessionManager([ServerReachabilityErrorInterceptor()])
|
|
||||||
..updateSettings(clientCertificate: clientCertificate)
|
..updateSettings(clientCertificate: clientCertificate)
|
||||||
..client.options.connectTimeout = const Duration(seconds: 5)
|
..client.options.connectTimeout = const Duration(seconds: 5)
|
||||||
..client.options.receiveTimeout = const Duration(seconds: 5);
|
..client.options.receiveTimeout = const Duration(seconds: 5);
|
||||||
@@ -84,8 +82,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
}
|
}
|
||||||
return ReachabilityStatus.notReachable;
|
return ReachabilityStatus.notReachable;
|
||||||
} on DioError catch (error) {
|
} on DioError catch (error) {
|
||||||
if (error.type == DioErrorType.unknown &&
|
if (error.type == DioErrorType.unknown && error.error is ReachabilityStatus) {
|
||||||
error.error is ReachabilityStatus) {
|
|
||||||
return error.error as ReachabilityStatus;
|
return error.error as ReachabilityStatus;
|
||||||
}
|
}
|
||||||
} on TlsException catch (error) {
|
} on TlsException catch (error) {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
|
|||||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
abstract class StatusService {
|
abstract class StatusService {
|
||||||
Future<void> startListeningBeforeDocumentUpload(String httpUrl,
|
Future<void> startListeningBeforeDocumentUpload(
|
||||||
AuthenticationInformation credentials, String documentFileName);
|
String httpUrl, UserCredentials credentials, String documentFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketStatusService implements StatusService {
|
class WebSocketStatusService implements StatusService {
|
||||||
@@ -25,7 +26,7 @@ class WebSocketStatusService implements StatusService {
|
|||||||
@override
|
@override
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
Future<void> startListeningBeforeDocumentUpload(
|
||||||
String httpUrl,
|
String httpUrl,
|
||||||
AuthenticationInformation credentials,
|
UserCredentials credentials,
|
||||||
String documentFileName,
|
String documentFileName,
|
||||||
) async {
|
) async {
|
||||||
// socket = await WebSocket.connect(
|
// socket = await WebSocket.connect(
|
||||||
@@ -57,7 +58,7 @@ class LongPollingStatusService implements StatusService {
|
|||||||
@override
|
@override
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
Future<void> startListeningBeforeDocumentUpload(
|
||||||
String httpUrl,
|
String httpUrl,
|
||||||
AuthenticationInformation credentials,
|
UserCredentials credentials,
|
||||||
String documentFileName,
|
String documentFileName,
|
||||||
) async {
|
) async {
|
||||||
// final today = DateTime.now();
|
// final today = DateTime.now();
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -42,8 +42,7 @@ class AppDrawer extends StatelessWidget {
|
|||||||
leading: const Icon(Icons.bug_report_outlined),
|
leading: const Icon(Icons.bug_report_outlined),
|
||||||
title: Text(S.of(context)!.reportABug),
|
title: Text(S.of(context)!.reportABug),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString('https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||||
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -69,7 +68,10 @@ class AppDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const SettingsPage(),
|
builder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<PaperlessServerInformationCubit>(),
|
||||||
|
child: const SettingsPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ class ApplicationIntroSlideshow extends StatefulWidget {
|
|||||||
const ApplicationIntroSlideshow({super.key});
|
const ApplicationIntroSlideshow({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ApplicationIntroSlideshow> createState() =>
|
State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState();
|
||||||
_ApplicationIntroSlideshowState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: INTL ALL
|
//TODO: INTL ALL
|
||||||
@@ -28,7 +27,9 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
|
|||||||
showDoneButton: true,
|
showDoneButton: true,
|
||||||
next: Text(S.of(context)!.next),
|
next: Text(S.of(context)!.next),
|
||||||
done: Text(S.of(context)!.done),
|
done: Text(S.of(context)!.done),
|
||||||
onDone: () => Navigator.pop(context),
|
onDone: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
dotsDecorator: DotsDecorator(
|
dotsDecorator: DotsDecorator(
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
@@ -44,9 +43,8 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
width: 16,
|
width: 16,
|
||||||
)
|
)
|
||||||
: const Icon(Icons.download),
|
: const Icon(Icons.download),
|
||||||
onPressed: widget.document != null && widget.enabled
|
onPressed:
|
||||||
? () => _onDownload(widget.document!)
|
widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null,
|
||||||
: null,
|
|
||||||
).paddedOnly(right: 4);
|
).paddedOnly(right: 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
await context.read<DocumentDetailsCubit>().downloadDocument(
|
await context.read<DocumentDetailsCubit>().downloadDocument(
|
||||||
downloadOriginal: downloadOriginal,
|
downloadOriginal: downloadOriginal,
|
||||||
locale: context.read<GlobalAppSettings>().preferredLocaleSubtag,
|
locale: context.read<GlobalSettings>().preferredLocaleSubtag,
|
||||||
);
|
);
|
||||||
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
|
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
|
||||||
as s;
|
|
||||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SliverSearchBar extends StatelessWidget {
|
class SliverSearchBar extends StatelessWidget {
|
||||||
@@ -37,8 +37,7 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
onPressed: Scaffold.of(context).openDrawer,
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
),
|
),
|
||||||
trailingIcon: IconButton(
|
trailingIcon: IconButton(
|
||||||
icon: BlocBuilder<PaperlessServerInformationCubit,
|
icon: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
|
||||||
PaperlessServerInformationState>(
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
child: Text(state.information?.userInitials ?? ''),
|
child: Text(state.information?.userInitials ?? ''),
|
||||||
@@ -48,7 +47,10 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const AccountSettingsDialog(),
|
builder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<PaperlessServerInformationCubit>(),
|
||||||
|
child: const ManageAccountsPage(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -46,9 +46,8 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
padding.bottom -
|
padding.bottom -
|
||||||
kBottomNavigationBarHeight -
|
kBottomNavigationBarHeight -
|
||||||
kToolbarHeight;
|
kToolbarHeight;
|
||||||
final maxHeight = highlights != null
|
final maxHeight =
|
||||||
? min(600.0, availableHeight)
|
highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight);
|
||||||
: min(500.0, availableHeight);
|
|
||||||
return Card(
|
return Card(
|
||||||
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
|
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
|||||||
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
|
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||||
@@ -185,6 +186,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final userId = context.watch<AuthenticationCubit>().state.userId;
|
||||||
final destinations = [
|
final destinations = [
|
||||||
RouteDescription(
|
RouteDescription(
|
||||||
icon: const Icon(Icons.description_outlined),
|
icon: const Icon(Icons.description_outlined),
|
||||||
@@ -232,19 +234,20 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
];
|
];
|
||||||
final routes = <Widget>[
|
final routes = <Widget>[
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
|
key: ValueKey(userId),
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => DocumentsCubit(
|
create: (context) => DocumentsCubit(
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
),
|
)..reload(),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => SavedViewCubit(
|
create: (context) => SavedViewCubit(
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
),
|
)..reload(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const DocumentsPage(),
|
child: const DocumentsPage(),
|
||||||
@@ -254,6 +257,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
child: const ScannerPage(),
|
child: const ScannerPage(),
|
||||||
),
|
),
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
|
key: ValueKey(userId),
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => LabelCubit(context.read()),
|
create: (context) => LabelCubit(context.read()),
|
||||||
@@ -266,12 +270,12 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
child: const InboxPage(),
|
child: const InboxPage(),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return MultiBlocListener(
|
return MultiBlocListener(
|
||||||
listeners: [
|
listeners: [
|
||||||
BlocListener<ConnectivityCubit, ConnectivityState>(
|
BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||||
//Only re-initialize data if the connectivity changed from not connected to connected
|
//Only re-initialize data if the connectivity changed from not connected to connected
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) => current == ConnectivityState.connected,
|
||||||
current == ConnectivityState.connected,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
_initializeData(context);
|
_initializeData(context);
|
||||||
},
|
},
|
||||||
@@ -280,9 +284,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.task != null) {
|
if (state.task != null) {
|
||||||
// Handle local notifications on task change (only when app is running for now).
|
// Handle local notifications on task change (only when app is running for now).
|
||||||
context
|
context.read<LocalNotificationService>().notifyTaskChanged(state.task!);
|
||||||
.read<LocalNotificationService>()
|
|
||||||
.notifyTaskChanged(state.task!);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -295,9 +297,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
NavigationRail(
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
destinations: destinations
|
destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(),
|
||||||
.map((e) => e.toNavigationRailDestination())
|
|
||||||
.toList(),
|
|
||||||
selectedIndex: _currentIndex,
|
selectedIndex: _currentIndex,
|
||||||
onDestinationSelected: _onNavigationChanged,
|
onDestinationSelected: _onNavigationChanged,
|
||||||
),
|
),
|
||||||
@@ -315,8 +315,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
selectedIndex: _currentIndex,
|
selectedIndex: _currentIndex,
|
||||||
onDestinationSelected: _onNavigationChanged,
|
onDestinationSelected: _onNavigationChanged,
|
||||||
destinations:
|
destinations: destinations.map((e) => e.toNavigationDestination()).toList(),
|
||||||
destinations.map((e) => e.toNavigationDestination()).toList(),
|
|
||||||
),
|
),
|
||||||
body: routes[_currentIndex],
|
body: routes[_currentIndex],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -33,9 +32,7 @@ class VerifyIdentityPage extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(S
|
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
|
||||||
.of(context)!
|
|
||||||
.useTheConfiguredBiometricFactorToAuthenticate)
|
|
||||||
.paddedSymmetrically(horizontal: 16),
|
.paddedSymmetrically(horizontal: 16),
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.fingerprint,
|
Icons.fingerprint,
|
||||||
@@ -57,9 +54,7 @@ class VerifyIdentityPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => context
|
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
|
||||||
.read<AuthenticationCubit>()
|
|
||||||
.restoreSessionState(),
|
|
||||||
child: Text(S.of(context)!.verifyIdentity),
|
child: Text(S.of(context)!.verifyIdentity),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/document_pag
|
|||||||
part 'inbox_cubit.g.dart';
|
part 'inbox_cubit.g.dart';
|
||||||
part 'inbox_state.dart';
|
part 'inbox_state.dart';
|
||||||
|
|
||||||
class InboxCubit extends HydratedCubit<InboxState>
|
class InboxCubit extends HydratedCubit<InboxState> with DocumentPagingBlocMixin {
|
||||||
with DocumentPagingBlocMixin {
|
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
|
|
||||||
final PaperlessDocumentsApi _documentsApi;
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
@@ -37,10 +36,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
this,
|
this,
|
||||||
onDeleted: remove,
|
onDeleted: remove,
|
||||||
onUpdated: (document) {
|
onUpdated: (document) {
|
||||||
if (document.tags
|
if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) {
|
||||||
.toSet()
|
|
||||||
.intersection(state.inboxTags.toSet())
|
|
||||||
.isEmpty) {
|
|
||||||
remove(document);
|
remove(document);
|
||||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||||
} else {
|
} else {
|
||||||
@@ -76,7 +72,9 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
/// Fetches inbox tag ids and loads the inbox items (documents).
|
/// Fetches inbox tag ids and loads the inbox items (documents).
|
||||||
///
|
///
|
||||||
Future<void> loadInbox() async {
|
Future<void> loadInbox() async {
|
||||||
|
if (!isClosed) {
|
||||||
debugPrint("Initializing inbox...");
|
debugPrint("Initializing inbox...");
|
||||||
|
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
@@ -91,6 +89,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(inboxTags: inboxTags));
|
emit(state.copyWith(inboxTags: inboxTags));
|
||||||
updateFilter(
|
updateFilter(
|
||||||
filter: DocumentFilter(
|
filter: DocumentFilter(
|
||||||
@@ -99,6 +98,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Fetches inbox tag ids and loads the inbox items (documents).
|
/// Fetches inbox tag ids and loads the inbox items (documents).
|
||||||
@@ -133,8 +133,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
/// from the inbox.
|
/// from the inbox.
|
||||||
///
|
///
|
||||||
Future<Iterable<int>> removeFromInbox(DocumentModel document) async {
|
Future<Iterable<int>> removeFromInbox(DocumentModel document) async {
|
||||||
final tagsToRemove =
|
final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet());
|
||||||
document.tags.toSet().intersection(state.inboxTags.toSet());
|
|
||||||
|
|
||||||
final updatedTags = {...document.tags}..removeAll(tagsToRemove);
|
final updatedTags = {...document.tags}..removeAll(tagsToRemove);
|
||||||
final updatedDocument = await api.update(
|
final updatedDocument = await api.update(
|
||||||
@@ -188,8 +187,8 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
Future<void> assignAsn(DocumentModel document) async {
|
Future<void> assignAsn(DocumentModel document) async {
|
||||||
if (document.archiveSerialNumber == null) {
|
if (document.archiveSerialNumber == null) {
|
||||||
final int asn = await _documentsApi.findNextAsn();
|
final int asn = await _documentsApi.findNextAsn();
|
||||||
final updatedDocument = await _documentsApi
|
final updatedDocument =
|
||||||
.update(document.copyWith(archiveSerialNumber: () => asn));
|
await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn));
|
||||||
|
|
||||||
replace(updatedDocument);
|
replace(updatedDocument);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/user_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
part 'authentication_state.dart';
|
part 'authentication_state.dart';
|
||||||
|
|
||||||
@@ -20,15 +25,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
final LocalAuthenticationService _localAuthService;
|
final LocalAuthenticationService _localAuthService;
|
||||||
final PaperlessAuthenticationApi _authApi;
|
final PaperlessAuthenticationApi _authApi;
|
||||||
final SessionManager _dioWrapper;
|
final SessionManager _dioWrapper;
|
||||||
|
final LabelRepository _labelRepository;
|
||||||
|
final SavedViewRepository _savedViewRepository;
|
||||||
|
final PaperlessServerStatsApi _serverStatsApi;
|
||||||
|
|
||||||
AuthenticationCubit(
|
AuthenticationCubit(
|
||||||
this._localAuthService,
|
this._localAuthService,
|
||||||
this._authApi,
|
this._authApi,
|
||||||
this._dioWrapper,
|
this._dioWrapper,
|
||||||
) : super(AuthenticationState.initial);
|
this._labelRepository,
|
||||||
|
this._savedViewRepository,
|
||||||
|
this._serverStatsApi,
|
||||||
|
) : super(const AuthenticationState());
|
||||||
|
|
||||||
Future<void> login({
|
Future<void> login({
|
||||||
required UserCredentials credentials,
|
required LoginFormCredentials credentials,
|
||||||
required String serverUrl,
|
required String serverUrl,
|
||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -47,107 +58,239 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
clientCertificate: clientCertificate,
|
clientCertificate: clientCertificate,
|
||||||
authToken: token,
|
authToken: token,
|
||||||
);
|
);
|
||||||
final authInfo = AuthenticationInformation(
|
|
||||||
username: credentials.username!,
|
|
||||||
serverUrl: serverUrl,
|
|
||||||
clientCertificate: clientCertificate,
|
|
||||||
token: token,
|
|
||||||
);
|
|
||||||
final userId = "${credentials.username}@$serverUrl";
|
final userId = "${credentials.username}@$serverUrl";
|
||||||
|
|
||||||
// Mark logged in user as currently active user.
|
// If it is first time login, create settings for this user.
|
||||||
final globalSettings = GlobalAppSettings.boxedValue;
|
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
|
||||||
globalSettings.currentLoggedInUser = userId;
|
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||||
await globalSettings.save();
|
if (!userSettingsBox.containsKey(userId)) {
|
||||||
|
userSettingsBox.put(userId, UserSettings());
|
||||||
|
}
|
||||||
|
final fullName = await _fetchFullName();
|
||||||
|
|
||||||
// Save credentials in encrypted box
|
if (!userAccountBox.containsKey(userId)) {
|
||||||
final encryptedBox = await _openEncryptedBox();
|
userAccountBox.put(
|
||||||
await encryptedBox.put(
|
|
||||||
userId,
|
userId,
|
||||||
authInfo,
|
UserAccount(
|
||||||
);
|
serverUrl: serverUrl,
|
||||||
encryptedBox.close();
|
username: credentials.username!,
|
||||||
|
fullName: fullName,
|
||||||
emit(
|
|
||||||
AuthenticationState(
|
|
||||||
wasLoginStored: false,
|
|
||||||
authentication: authInfo,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark logged in user as currently active user.
|
||||||
|
final globalSettings = GlobalSettings.boxedValue;
|
||||||
|
globalSettings.currentLoggedInUser = userId;
|
||||||
|
globalSettings.save();
|
||||||
|
|
||||||
|
// Save credentials in encrypted box
|
||||||
|
final userCredentialsBox = await _getUserCredentialsBox();
|
||||||
|
await userCredentialsBox.put(
|
||||||
|
userId,
|
||||||
|
UserCredentials(
|
||||||
|
token: token,
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
userCredentialsBox.close();
|
||||||
|
emit(
|
||||||
|
AuthenticationState(
|
||||||
|
isAuthenticated: true,
|
||||||
|
username: credentials.username,
|
||||||
|
userId: userId,
|
||||||
|
fullName: fullName,
|
||||||
|
//TODO: Query ui settings with full name and add as parameter here...
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switches to another account if it exists.
|
||||||
|
Future<void> switchAccount(String userId) async {
|
||||||
|
final globalSettings = GlobalSettings.boxedValue;
|
||||||
|
if (globalSettings.currentLoggedInUser == userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||||
|
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
|
||||||
|
|
||||||
|
if (!userSettingsBox.containsKey(userId)) {
|
||||||
|
debugPrint("User $userId not yet registered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userSettings = userSettingsBox.get(userId)!;
|
||||||
|
final account = userAccountBox.get(userId)!;
|
||||||
|
|
||||||
|
if (userSettings.isBiometricAuthenticationEnabled) {
|
||||||
|
final authenticated =
|
||||||
|
await _localAuthService.authenticateLocalUser("Authenticate to switch your account.");
|
||||||
|
if (!authenticated) {
|
||||||
|
debugPrint("User unable to authenticate.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final credentialsBox = await _getUserCredentialsBox();
|
||||||
|
if (!credentialsBox.containsKey(userId)) {
|
||||||
|
await credentialsBox.close();
|
||||||
|
debugPrint("Invalid authentication for $userId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final credentials = credentialsBox.get(userId);
|
||||||
|
await _resetExternalState();
|
||||||
|
|
||||||
|
_dioWrapper.updateSettings(
|
||||||
|
authToken: credentials!.token,
|
||||||
|
clientCertificate: credentials.clientCertificate,
|
||||||
|
serverInformation: PaperlessServerInformationModel(),
|
||||||
|
baseUrl: account.serverUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
globalSettings.currentLoggedInUser = userId;
|
||||||
|
await globalSettings.save();
|
||||||
|
await _reloadRepositories();
|
||||||
|
emit(
|
||||||
|
AuthenticationState(
|
||||||
|
isAuthenticated: true,
|
||||||
|
username: account.username,
|
||||||
|
fullName: account.fullName,
|
||||||
|
userId: userId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> addAccount({
|
||||||
|
required LoginFormCredentials credentials,
|
||||||
|
required String serverUrl,
|
||||||
|
ClientCertificate? clientCertificate,
|
||||||
|
required bool enableBiometricAuthentication,
|
||||||
|
}) async {
|
||||||
|
assert(credentials.password != null && credentials.username != null);
|
||||||
|
final userId = "${credentials.username}@$serverUrl";
|
||||||
|
final userAccountsBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||||
|
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
|
||||||
|
|
||||||
|
if (userAccountsBox.containsKey(userId)) {
|
||||||
|
throw Exception("User already exists");
|
||||||
|
}
|
||||||
|
// Creates a parallel session to get token and disposes of resources after.
|
||||||
|
final sessionManager = SessionManager([
|
||||||
|
DioHttpErrorInterceptor(),
|
||||||
|
]);
|
||||||
|
sessionManager.updateSettings(
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
baseUrl: serverUrl,
|
||||||
|
);
|
||||||
|
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
|
||||||
|
|
||||||
|
final token = await authApi.login(
|
||||||
|
username: credentials.username!,
|
||||||
|
password: credentials.password!,
|
||||||
|
);
|
||||||
|
sessionManager.resetSettings();
|
||||||
|
await userSettingsBox.put(
|
||||||
|
userId,
|
||||||
|
UserSettings(
|
||||||
|
isBiometricAuthenticationEnabled: enableBiometricAuthentication,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final fullName = await _fetchFullName();
|
||||||
|
await userAccountsBox.put(
|
||||||
|
userId,
|
||||||
|
UserAccount(
|
||||||
|
serverUrl: serverUrl,
|
||||||
|
username: credentials.username!,
|
||||||
|
fullName: fullName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final userCredentialsBox = await _getUserCredentialsBox();
|
||||||
|
await userCredentialsBox.put(
|
||||||
|
userId,
|
||||||
|
UserCredentials(
|
||||||
|
token: token,
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await userCredentialsBox.close();
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeAccount(String userId) async {
|
||||||
|
final globalSettings = GlobalSettings.boxedValue;
|
||||||
|
final currentUser = globalSettings.currentLoggedInUser;
|
||||||
|
final userAccountBox = Hive.box<UserAccount>(HiveBoxes.userAccount);
|
||||||
|
final userCredentialsBox = await _getUserCredentialsBox();
|
||||||
|
final userSettingsBox = Hive.box<UserSettings>(HiveBoxes.userSettings);
|
||||||
|
|
||||||
|
await userAccountBox.delete(userId);
|
||||||
|
await userCredentialsBox.delete(userId);
|
||||||
|
await userSettingsBox.delete(userId);
|
||||||
|
|
||||||
|
if (currentUser == userId) {
|
||||||
|
return logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Performs a conditional hydration based on the local authentication success.
|
/// Performs a conditional hydration based on the local authentication success.
|
||||||
///
|
///
|
||||||
Future<void> restoreSessionState() async {
|
Future<void> restoreSessionState() async {
|
||||||
final globalSettings = GlobalAppSettings.boxedValue;
|
final globalSettings = GlobalSettings.boxedValue;
|
||||||
if (globalSettings.currentLoggedInUser == null) {
|
final userId = globalSettings.currentLoggedInUser;
|
||||||
|
if (userId == null) {
|
||||||
// If there is nothing to restore, we can quit here.
|
// If there is nothing to restore, we can quit here.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final userSettings = Hive.box<UserAppSettings>(HiveBoxes.userSettings)
|
final userSettings = Hive.box<UserSettings>(HiveBoxes.userSettings).get(userId)!;
|
||||||
.get(globalSettings.currentLoggedInUser!);
|
final userAccount = Hive.box<UserAccount>(HiveBoxes.userAccount).get(userId)!;
|
||||||
|
|
||||||
if (userSettings!.isBiometricAuthenticationEnabled) {
|
if (userSettings.isBiometricAuthenticationEnabled) {
|
||||||
final localAuthSuccess = await _localAuthService
|
final localAuthSuccess =
|
||||||
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
|
await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
|
||||||
if (localAuthSuccess) {
|
if (!localAuthSuccess) {
|
||||||
final authentication = await _readAuthenticationFromEncryptedBox(
|
emit(const AuthenticationState(showBiometricAuthenticationScreen: true));
|
||||||
globalSettings.currentLoggedInUser!);
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final userCredentialsBox = await _getUserCredentialsBox();
|
||||||
|
|
||||||
|
final authentication = userCredentialsBox.get(globalSettings.currentLoggedInUser!);
|
||||||
if (authentication != null) {
|
if (authentication != null) {
|
||||||
_dioWrapper.updateSettings(
|
_dioWrapper.updateSettings(
|
||||||
clientCertificate: authentication.clientCertificate,
|
clientCertificate: authentication.clientCertificate,
|
||||||
authToken: authentication.token,
|
authToken: authentication.token,
|
||||||
baseUrl: authentication.serverUrl,
|
baseUrl: userAccount.serverUrl,
|
||||||
);
|
serverInformation: PaperlessServerInformationModel(),
|
||||||
return emit(
|
|
||||||
AuthenticationState(
|
|
||||||
wasLoginStored: true,
|
|
||||||
authentication: state.authentication,
|
|
||||||
wasLocalAuthenticationSuccessful: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return emit(
|
|
||||||
AuthenticationState(
|
|
||||||
wasLoginStored: true,
|
|
||||||
wasLocalAuthenticationSuccessful: false,
|
|
||||||
authentication: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final authentication = await _readAuthenticationFromEncryptedBox(
|
|
||||||
globalSettings.currentLoggedInUser!);
|
|
||||||
if (authentication != null) {
|
|
||||||
_dioWrapper.updateSettings(
|
|
||||||
clientCertificate: authentication.clientCertificate,
|
|
||||||
authToken: authentication.token,
|
|
||||||
baseUrl: authentication.serverUrl,
|
|
||||||
);
|
);
|
||||||
emit(
|
emit(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
authentication: authentication,
|
isAuthenticated: true,
|
||||||
wasLoginStored: true,
|
showBiometricAuthenticationScreen: false,
|
||||||
|
username: userAccount.username,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return emit(AuthenticationState.initial);
|
throw Exception("User should be authenticated but no authentication information was found.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthenticationInformation?> _readAuthenticationFromEncryptedBox(
|
Future<void> logout() async {
|
||||||
String userId) {
|
await _resetExternalState();
|
||||||
return _openEncryptedBox().then((box) => box.get(userId));
|
final globalSettings = GlobalSettings.boxedValue;
|
||||||
|
globalSettings
|
||||||
|
..currentLoggedInUser = null
|
||||||
|
..save();
|
||||||
|
emit(const AuthenticationState());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Box<AuthenticationInformation?>> _openEncryptedBox() async {
|
Future<Uint8List> _getEncryptedBoxKey() async {
|
||||||
const secureStorage = FlutterSecureStorage();
|
const secureStorage = FlutterSecureStorage();
|
||||||
final encryptionKeyString = await secureStorage.read(key: 'key');
|
if (!await secureStorage.containsKey(key: 'key')) {
|
||||||
if (encryptionKeyString == null) {
|
|
||||||
final key = Hive.generateSecureKey();
|
final key = Hive.generateSecureKey();
|
||||||
|
|
||||||
await secureStorage.write(
|
await secureStorage.write(
|
||||||
@@ -155,17 +298,40 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
value: base64UrlEncode(key),
|
value: base64UrlEncode(key),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final key = await secureStorage.read(key: 'key');
|
final key = (await secureStorage.read(key: 'key'))!;
|
||||||
final encryptionKeyUint8List = base64Url.decode(key!);
|
return base64Decode(key);
|
||||||
return await Hive.openBox<AuthenticationInformation>(
|
}
|
||||||
HiveBoxes.vault,
|
|
||||||
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
|
Future<Box<UserCredentials>> _getUserCredentialsBox() async {
|
||||||
|
final keyBytes = await _getEncryptedBoxKey();
|
||||||
|
return Hive.openBox<UserCredentials>(
|
||||||
|
HiveBoxes.userCredentials,
|
||||||
|
encryptionCipher: HiveAesCipher(keyBytes),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> _resetExternalState() {
|
||||||
await Hive.box<AuthenticationInformation>(HiveBoxes.authentication).clear();
|
|
||||||
_dioWrapper.resetSettings();
|
_dioWrapper.resetSettings();
|
||||||
emit(AuthenticationState.initial);
|
return Future.wait([
|
||||||
|
HydratedBloc.storage.clear(),
|
||||||
|
_labelRepository.clear(),
|
||||||
|
_savedViewRepository.clear(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reloadRepositories() {
|
||||||
|
return Future.wait([
|
||||||
|
_labelRepository.initialize(),
|
||||||
|
_savedViewRepository.findAll(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _fetchFullName() async {
|
||||||
|
try {
|
||||||
|
final uiSettings = await _serverStatsApi.getUiSettings();
|
||||||
|
return uiSettings.displayName;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,43 @@
|
|||||||
part of 'authentication_cubit.dart';
|
part of 'authentication_cubit.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
class AuthenticationState with EquatableMixin {
|
||||||
class AuthenticationState {
|
final bool showBiometricAuthenticationScreen;
|
||||||
final bool wasLoginStored;
|
final bool isAuthenticated;
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
final String? username;
|
||||||
final bool? wasLocalAuthenticationSuccessful;
|
final String? fullName;
|
||||||
final AuthenticationInformation? authentication;
|
final String? userId;
|
||||||
|
|
||||||
static final AuthenticationState initial = AuthenticationState(
|
const AuthenticationState({
|
||||||
wasLoginStored: false,
|
this.isAuthenticated = false,
|
||||||
);
|
this.showBiometricAuthenticationScreen = false,
|
||||||
|
this.username,
|
||||||
bool get isAuthenticated => authentication != null;
|
this.fullName,
|
||||||
|
this.userId,
|
||||||
AuthenticationState({
|
|
||||||
required this.wasLoginStored,
|
|
||||||
this.wasLocalAuthenticationSuccessful,
|
|
||||||
this.authentication,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AuthenticationState copyWith({
|
AuthenticationState copyWith({
|
||||||
bool? wasLoginStored,
|
|
||||||
bool? isAuthenticated,
|
bool? isAuthenticated,
|
||||||
AuthenticationInformation? authentication,
|
bool? showBiometricAuthenticationScreen,
|
||||||
bool? wasLocalAuthenticationSuccessful,
|
String? username,
|
||||||
|
String? fullName,
|
||||||
|
String? userId,
|
||||||
}) {
|
}) {
|
||||||
return AuthenticationState(
|
return AuthenticationState(
|
||||||
wasLoginStored: wasLoginStored ?? this.wasLoginStored,
|
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||||
authentication: authentication ?? this.authentication,
|
showBiometricAuthenticationScreen:
|
||||||
wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ??
|
showBiometricAuthenticationScreen ?? this.showBiometricAuthenticationScreen,
|
||||||
this.wasLocalAuthenticationSuccessful,
|
username: username ?? this.username,
|
||||||
|
fullName: fullName ?? this.fullName,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
fullName,
|
||||||
|
isAuthenticated,
|
||||||
|
showBiometricAuthenticationScreen,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,44 +3,15 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/type/types.dart';
|
|
||||||
|
|
||||||
part 'client_certificate.g.dart';
|
part 'client_certificate.g.dart';
|
||||||
|
|
||||||
@HiveType(typeId: HiveTypeIds.clientCertificate)
|
@HiveType(typeId: HiveTypeIds.clientCertificate)
|
||||||
class ClientCertificate {
|
class ClientCertificate {
|
||||||
static const bytesKey = 'bytes';
|
|
||||||
static const passphraseKey = 'passphrase';
|
|
||||||
|
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final Uint8List bytes;
|
Uint8List bytes;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
final String? passphrase;
|
String? passphrase;
|
||||||
|
|
||||||
ClientCertificate({required this.bytes, this.passphrase});
|
ClientCertificate({required this.bytes, this.passphrase});
|
||||||
|
|
||||||
static ClientCertificate? nullable(Uint8List? bytes, {String? passphrase}) {
|
|
||||||
if (bytes != null) {
|
|
||||||
return ClientCertificate(bytes: bytes, passphrase: passphrase);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
JSON toJson() {
|
|
||||||
return {
|
|
||||||
bytesKey: base64Encode(bytes),
|
|
||||||
passphraseKey: passphrase,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientCertificate.fromJson(JSON json)
|
|
||||||
: bytes = base64Decode(json[bytesKey]),
|
|
||||||
passphrase = json[passphraseKey];
|
|
||||||
|
|
||||||
ClientCertificate copyWith({Uint8List? bytes, String? passphrase}) {
|
|
||||||
return ClientCertificate(
|
|
||||||
bytes: bytes ?? this.bytes,
|
|
||||||
passphrase: passphrase ?? this.passphrase,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/type/types.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
|
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
import 'widgets/login_pages/server_login_page.dart';
|
import 'widgets/login_pages/server_login_page.dart';
|
||||||
import 'widgets/never_scrollable_scroll_behavior.dart';
|
import 'widgets/never_scrollable_scroll_behavior.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({Key? key}) : super(key: key);
|
final void Function(
|
||||||
|
BuildContext context,
|
||||||
|
String username,
|
||||||
|
String password,
|
||||||
|
String serverUrl,
|
||||||
|
ClientCertificate? clientCertificate,
|
||||||
|
) onSubmit;
|
||||||
|
|
||||||
|
final String submitText;
|
||||||
|
|
||||||
|
const LoginPage({
|
||||||
|
Key? key,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.submitText,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LoginPage> createState() => _LoginPageState();
|
State<LoginPage> createState() => _LoginPageState();
|
||||||
@@ -46,7 +65,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
ServerLoginPage(
|
ServerLoginPage(
|
||||||
formBuilderKey: _formKey,
|
formBuilderKey: _formKey,
|
||||||
onDone: _login,
|
submitText: widget.submitText,
|
||||||
|
onSubmit: _login,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -58,24 +78,23 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
final form = _formKey.currentState!.value;
|
final form = _formKey.currentState!.value;
|
||||||
try {
|
ClientCertificate? clientCert;
|
||||||
await context.read<AuthenticationCubit>().login(
|
final clientCertFormModel =
|
||||||
credentials: form[UserCredentialsFormField.fkCredentials],
|
form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?;
|
||||||
serverUrl: form[ServerAddressFormField.fkServerAddress],
|
if (clientCertFormModel != null) {
|
||||||
clientCertificate:
|
clientCert = ClientCertificate(
|
||||||
form[ClientCertificateFormField.fkClientCertificate],
|
bytes: clientCertFormModel.bytes,
|
||||||
|
passphrase: clientCertFormModel.passphrase,
|
||||||
);
|
);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
} on PaperlessValidationErrors catch (error, stackTrace) {
|
|
||||||
if (error.hasFieldUnspecificError) {
|
|
||||||
showLocalizedError(context, error.fieldUnspecificError!);
|
|
||||||
} else {
|
|
||||||
showGenericError(context, error.values.first, stackTrace);
|
|
||||||
}
|
|
||||||
} catch (unknownError, stackTrace) {
|
|
||||||
showGenericError(context, unknownError.toString(), stackTrace);
|
|
||||||
}
|
}
|
||||||
|
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
||||||
|
widget.onSubmit(
|
||||||
|
context,
|
||||||
|
credentials.username!,
|
||||||
|
credentials.password!,
|
||||||
|
form[ServerAddressFormField.fkServerAddress],
|
||||||
|
clientCert,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
@@ -15,23 +16,21 @@ import 'obscured_input_text_form_field.dart';
|
|||||||
class ClientCertificateFormField extends StatefulWidget {
|
class ClientCertificateFormField extends StatefulWidget {
|
||||||
static const fkClientCertificate = 'clientCertificate';
|
static const fkClientCertificate = 'clientCertificate';
|
||||||
|
|
||||||
final void Function(ClientCertificate? cert) onChanged;
|
final void Function(ClientCertificateFormModel? cert) onChanged;
|
||||||
const ClientCertificateFormField({
|
const ClientCertificateFormField({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ClientCertificateFormField> createState() =>
|
State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState();
|
||||||
_ClientCertificateFormFieldState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ClientCertificateFormFieldState
|
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> {
|
||||||
extends State<ClientCertificateFormField> {
|
|
||||||
File? _selectedFile;
|
File? _selectedFile;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderField<ClientCertificate?>(
|
return FormBuilderField<ClientCertificateFormModel?>(
|
||||||
key: const ValueKey('login-client-cert'),
|
key: const ValueKey('login-client-cert'),
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
@@ -46,8 +45,7 @@ class _ClientCertificateFormFieldState
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
final theme =
|
final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
|
||||||
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
|
|
||||||
return Theme(
|
return Theme(
|
||||||
data: theme,
|
data: theme,
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
@@ -124,7 +122,7 @@ class _ClientCertificateFormFieldState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSelectFile(FormFieldState<ClientCertificate?> field) async {
|
Future<void> _onSelectFile(FormFieldState<ClientCertificateFormModel?> field) async {
|
||||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
);
|
);
|
||||||
@@ -133,14 +131,13 @@ class _ClientCertificateFormFieldState
|
|||||||
setState(() {
|
setState(() {
|
||||||
_selectedFile = file;
|
_selectedFile = file;
|
||||||
});
|
});
|
||||||
final changedValue =
|
final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
||||||
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
ClientCertificateFormModel(bytes: file.readAsBytesSync());
|
||||||
ClientCertificate(bytes: file.readAsBytesSync());
|
|
||||||
field.didChange(changedValue);
|
field.didChange(changedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelectedFileText(FormFieldState<ClientCertificate?> field) {
|
Widget _buildSelectedFileText(FormFieldState<ClientCertificateFormModel?> field) {
|
||||||
if (field.value == null) {
|
if (field.value == null) {
|
||||||
assert(_selectedFile == null);
|
assert(_selectedFile == null);
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -14,14 +14,13 @@ class UserCredentialsFormField extends StatefulWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UserCredentialsFormField> createState() =>
|
State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState();
|
||||||
_UserCredentialsFormFieldState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderField<UserCredentials?>(
|
return FormBuilderField<LoginFormCredentials?>(
|
||||||
name: UserCredentialsFormField.fkCredentials,
|
name: UserCredentialsFormField.fkCredentials,
|
||||||
builder: (field) => AutofillGroup(
|
builder: (field) => AutofillGroup(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -34,7 +33,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
onChanged: (username) => field.didChange(
|
onChanged: (username) => field.didChange(
|
||||||
field.value?.copyWith(username: username) ??
|
field.value?.copyWith(username: username) ??
|
||||||
UserCredentials(username: username),
|
LoginFormCredentials(username: username),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value?.trim().isEmpty ?? true) {
|
if (value?.trim().isEmpty ?? true) {
|
||||||
@@ -51,7 +50,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
label: S.of(context)!.password,
|
label: S.of(context)!.password,
|
||||||
onChanged: (password) => field.didChange(
|
onChanged: (password) => field.didChange(
|
||||||
field.value?.copyWith(password: password) ??
|
field.value?.copyWith(password: password) ??
|
||||||
UserCredentials(password: password),
|
LoginFormCredentials(password: password),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value?.trim().isEmpty ?? true) {
|
if (value?.trim().isEmpty ?? true) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||||
@@ -35,9 +37,8 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|||||||
toolbarHeight: kToolbarHeight - 4,
|
toolbarHeight: kToolbarHeight - 4,
|
||||||
title: Text(S.of(context)!.connectToPaperless),
|
title: Text(S.of(context)!.connectToPaperless),
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
child: _isCheckingConnection
|
child:
|
||||||
? const LinearProgressIndicator()
|
_isCheckingConnection ? const LinearProgressIndicator() : const SizedBox(height: 4.0),
|
||||||
: const SizedBox(height: 4.0),
|
|
||||||
preferredSize: const Size.fromHeight(4.0),
|
preferredSize: const Size.fromHeight(4.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -67,9 +68,8 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: Text(S.of(context)!.continueLabel),
|
child: Text(S.of(context)!.continueLabel),
|
||||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
|
onPressed:
|
||||||
? widget.onContinue
|
_reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -81,16 +81,16 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isCheckingConnection = true;
|
_isCheckingConnection = true;
|
||||||
});
|
});
|
||||||
|
final certForm = widget.formBuilderKey.currentState
|
||||||
final status = await context
|
?.getRawValue(ClientCertificateFormField.fkClientCertificate)
|
||||||
.read<ConnectivityStatusService>()
|
as ClientCertificateFormModel?;
|
||||||
.isPaperlessServerReachable(
|
final status = await context.read<ConnectivityStatusService>().isPaperlessServerReachable(
|
||||||
address ??
|
address ??
|
||||||
widget.formBuilderKey.currentState!
|
widget.formBuilderKey.currentState!
|
||||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||||
widget.formBuilderKey.currentState?.getRawValue(
|
certForm != null
|
||||||
ClientCertificateFormField.fkClientCertificate,
|
? ClientCertificate(bytes: certForm.bytes, passphrase: certForm.passphrase)
|
||||||
),
|
: null,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isCheckingConnection = false;
|
_isCheckingConnection = false;
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr
|
|||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ServerLoginPage extends StatefulWidget {
|
class ServerLoginPage extends StatefulWidget {
|
||||||
final Future<void> Function() onDone;
|
final String submitText;
|
||||||
|
final Future<void> Function() onSubmit;
|
||||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
final GlobalKey<FormBuilderState> formBuilderKey;
|
||||||
const ServerLoginPage({
|
const ServerLoginPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onDone,
|
required this.onSubmit,
|
||||||
required this.formBuilderKey,
|
required this.formBuilderKey,
|
||||||
|
required this.submitText,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -23,8 +25,7 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverAddress = (widget.formBuilderKey.currentState
|
final serverAddress = (widget.formBuilderKey.currentState
|
||||||
?.getRawValue(ServerAddressFormField.fkServerAddress)
|
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
|
||||||
as String?)
|
|
||||||
?.replaceAll(RegExp(r'https?://'), '') ??
|
?.replaceAll(RegExp(r'https?://'), '') ??
|
||||||
'';
|
'';
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -50,7 +51,7 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
|
|||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() => _isLoginLoading = true);
|
setState(() => _isLoginLoading = true);
|
||||||
await widget.onDone();
|
await widget.onSubmit();
|
||||||
setState(() => _isLoginLoading = false);
|
setState(() => _isLoginLoading = false);
|
||||||
},
|
},
|
||||||
child: Text(S.of(context)!.signIn),
|
child: Text(S.of(context)!.signIn),
|
||||||
|
|||||||
@@ -43,12 +43,14 @@ class SavedViewDetailsCubit extends HydratedCubit<SavedViewDetailsState>
|
|||||||
_labelRepository.addListener(
|
_labelRepository.addListener(
|
||||||
this,
|
this,
|
||||||
onChanged: (labels) {
|
onChanged: (labels) {
|
||||||
|
if (!isClosed) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
correspondents: labels.correspondents,
|
correspondents: labels.correspondents,
|
||||||
documentTypes: labels.documentTypes,
|
documentTypes: labels.documentTypes,
|
||||||
tags: labels.tags,
|
tags: labels.tags,
|
||||||
storagePaths: labels.storagePaths,
|
storagePaths: labels.storagePaths,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
updateFilter(filter: savedView.toDocumentFilter());
|
updateFilter(filter: savedView.toDocumentFilter());
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
|
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
|
||||||
as s;
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||||
|
|
||||||
typedef OpenSearchCallback = void Function(BuildContext context);
|
typedef OpenSearchCallback = void Function(BuildContext context);
|
||||||
|
|
||||||
@@ -47,8 +47,7 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
onPressed: Scaffold.of(context).openDrawer,
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
),
|
),
|
||||||
trailingIcon: IconButton(
|
trailingIcon: IconButton(
|
||||||
icon: BlocBuilder<PaperlessServerInformationCubit,
|
icon: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
|
||||||
PaperlessServerInformationState>(
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
child: Text(state.information?.userInitials ?? ''),
|
child: Text(state.information?.userInitials ?? ''),
|
||||||
@@ -58,7 +57,10 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const AccountSettingsDialog(),
|
builder: (context) => BlocProvider.value(
|
||||||
|
value: context.read<PaperlessServerInformationCubit>(),
|
||||||
|
child: const ManageAccountsPage(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
|
||||||
part 'global_app_settings.g.dart';
|
part 'global_settings.g.dart';
|
||||||
|
|
||||||
@HiveType(typeId: HiveTypeIds.globalSettings)
|
@HiveType(typeId: HiveTypeIds.globalSettings)
|
||||||
class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
|
class GlobalSettings with HiveObjectMixin {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
String preferredLocaleSubtag;
|
String preferredLocaleSubtag;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
|
|||||||
@HiveField(4)
|
@HiveField(4)
|
||||||
String? currentLoggedInUser;
|
String? currentLoggedInUser;
|
||||||
|
|
||||||
GlobalAppSettings({
|
GlobalSettings({
|
||||||
required this.preferredLocaleSubtag,
|
required this.preferredLocaleSubtag,
|
||||||
this.preferredThemeMode = ThemeMode.system,
|
this.preferredThemeMode = ThemeMode.system,
|
||||||
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
||||||
@@ -30,7 +30,6 @@ class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
|
|||||||
this.currentLoggedInUser,
|
this.currentLoggedInUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
static GlobalAppSettings get boxedValue =>
|
static GlobalSettings get boxedValue =>
|
||||||
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||||
.get(HiveBoxSingleValueKey.value)!;
|
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
|
||||||
|
|
||||||
part 'user_app_settings.g.dart';
|
part 'user_settings.g.dart';
|
||||||
|
|
||||||
@HiveType(typeId: HiveTypeIds.userSettings)
|
@HiveType(typeId: HiveTypeIds.userSettings)
|
||||||
class UserAppSettings with HiveObjectMixin {
|
class UserSettings with HiveObjectMixin {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
bool isBiometricAuthenticationEnabled;
|
bool isBiometricAuthenticationEnabled;
|
||||||
|
|
||||||
UserAppSettings({
|
UserSettings({
|
||||||
this.isBiometricAuthenticationEnabled = false,
|
this.isBiometricAuthenticationEnabled = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class AccountSettingsDialog extends StatelessWidget {
|
class AccountSettingsDialog extends StatelessWidget {
|
||||||
@@ -20,7 +20,9 @@ class AccountSettingsDialog extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return GlobalSettingsBuilder(builder: (context, globalSettings) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
insetPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -30,24 +32,29 @@ class AccountSettingsDialog extends StatelessWidget {
|
|||||||
const CloseButton(),
|
const CloseButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: BlocBuilder<PaperlessServerInformationCubit,
|
content: BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
|
||||||
PaperlessServerInformationState>(
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ExpansionTile(
|
ValueListenableBuilder(
|
||||||
|
valueListenable: Hive.box<UserAccount>(HiveBoxes.userAccount).listenable(),
|
||||||
|
builder: (context, box, _) {
|
||||||
|
// final currentUser = globalSettings.currentLoggedInUser;
|
||||||
|
final currentUser = null;
|
||||||
|
final accountIds =
|
||||||
|
box.keys.whereNot((element) => element == currentUser).toList();
|
||||||
|
final accounts = accountIds.map((id) => box.get(id)!).toList();
|
||||||
|
return ExpansionTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
child: Text(state.information?.userInitials ?? ''),
|
child: Text(state.information?.userInitials ?? ''),
|
||||||
),
|
),
|
||||||
title: Text(state.information?.username ?? ''),
|
title: Text(state.information?.username ?? ''),
|
||||||
subtitle: Text(state.information?.host ?? ''),
|
subtitle: Text(state.information?.host ?? ''),
|
||||||
children: const [
|
children:
|
||||||
HintCard(
|
accounts.map((account) => _buildAccountTile(account, true)).toList(),
|
||||||
hintText: "WIP: Coming soon with multi user support!",
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.person_add_rounded),
|
leading: const Icon(Icons.person_add_rounded),
|
||||||
@@ -77,17 +84,31 @@ class AccountSettingsDialog extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLogout(BuildContext context) async {
|
Future<void> _onLogout(BuildContext context) async {
|
||||||
try {
|
try {
|
||||||
await context.read<AuthenticationCubit>().logout();
|
await context.read<AuthenticationCubit>().logout();
|
||||||
await context.read<GlobalAppSettings>();
|
|
||||||
await context.read<LabelRepository>().clear();
|
|
||||||
await context.read<SavedViewRepository>().clear();
|
|
||||||
await HydratedBloc.storage.clear();
|
await HydratedBloc.storage.clear();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAccountTile(UserAccount account, bool isActive) {
|
||||||
|
return ListTile(
|
||||||
|
selected: isActive,
|
||||||
|
title: Text(account.username),
|
||||||
|
subtitle: Text(account.serverUrl),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child: Text((account.fullName ?? account.username)
|
||||||
|
.split(" ")
|
||||||
|
.take(2)
|
||||||
|
.map((e) => e.substring(0, 1))
|
||||||
|
.map((e) => e.toUpperCase())
|
||||||
|
.join(" ")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
@@ -18,14 +15,13 @@ class SettingsPage extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(S.of(context)!.settings),
|
title: Text(S.of(context)!.settings),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: BlocBuilder<PaperlessServerInformationCubit,
|
bottomNavigationBar:
|
||||||
PaperlessServerInformationState>(
|
BlocBuilder<PaperlessServerInformationCubit, PaperlessServerInformationState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final info = state.information!;
|
final info = state.information!;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
S.of(context)!.loggedInAs(info.username ?? 'unknown') +
|
S.of(context)!.loggedInAs(info.username ?? 'unknown') + "@${info.host}",
|
||||||
"@${info.host}",
|
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import 'package:paperless_mobile/constants.dart';
|
|||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
|
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
@@ -37,8 +36,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
|
|||||||
options: [
|
options: [
|
||||||
RadioOption(
|
RadioOption(
|
||||||
value: ColorSchemeOption.classic,
|
value: ColorSchemeOption.classic,
|
||||||
label: translateColorSchemeOption(
|
label: translateColorSchemeOption(context, ColorSchemeOption.classic),
|
||||||
context, ColorSchemeOption.classic),
|
|
||||||
),
|
),
|
||||||
RadioOption(
|
RadioOption(
|
||||||
value: ColorSchemeOption.dynamic,
|
value: ColorSchemeOption.dynamic,
|
||||||
@@ -71,8 +69,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
|
|||||||
|
|
||||||
bool _isBelowAndroid12() {
|
bool _isBelowAndroid12() {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final int version =
|
final int version = int.tryParse(androidInfo!.version.release ?? '0') ?? 0;
|
||||||
int.tryParse(androidInfo!.version.release ?? '0') ?? 0;
|
|
||||||
return version < 12;
|
return version < 12;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,21 +3,18 @@ import 'package:flutter/src/widgets/framework.dart';
|
|||||||
import 'package:flutter/src/widgets/placeholder.dart';
|
import 'package:flutter/src/widgets/placeholder.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
|
|
||||||
class GlobalSettingsBuilder extends StatelessWidget {
|
class GlobalSettingsBuilder extends StatelessWidget {
|
||||||
|
final Widget Function(BuildContext context, GlobalSettings settings) builder;
|
||||||
final Widget Function(BuildContext context, GlobalAppSettings settings)
|
|
||||||
builder;
|
|
||||||
const GlobalSettingsBuilder({super.key, required this.builder});
|
const GlobalSettingsBuilder({super.key, required this.builder});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable:
|
valueListenable: Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
|
||||||
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings).listenable(),
|
|
||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
final settings = value.get(HiveBoxSingleValueKey.value)!;
|
final settings = value.getValue()!;
|
||||||
return builder(context, settings);
|
return builder(context, settings);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
@@ -11,8 +10,7 @@ class LanguageSelectionSetting extends StatefulWidget {
|
|||||||
const LanguageSelectionSetting({super.key});
|
const LanguageSelectionSetting({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LanguageSelectionSetting> createState() =>
|
State<LanguageSelectionSetting> createState() => _LanguageSelectionSettingState();
|
||||||
_LanguageSelectionSettingState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -14,8 +13,7 @@ class ThemeModeSetting extends StatelessWidget {
|
|||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(S.of(context)!.appearance),
|
title: Text(S.of(context)!.appearance),
|
||||||
subtitle: Text(_mapThemeModeToLocalizedString(
|
subtitle: Text(_mapThemeModeToLocalizedString(settings.preferredThemeMode, context)),
|
||||||
settings.preferredThemeMode, context)),
|
|
||||||
onTap: () => showDialog<ThemeMode>(
|
onTap: () => showDialog<ThemeMode>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => RadioSettingsDialog<ThemeMode>(
|
builder: (_) => RadioSettingsDialog<ThemeMode>(
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||||
|
|
||||||
class UserSettingsBuilder extends StatelessWidget {
|
class UserSettingsBuilder extends StatelessWidget {
|
||||||
final Widget Function(
|
final Widget Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
UserAppSettings? settings,
|
UserSettings? settings,
|
||||||
) builder;
|
) builder;
|
||||||
|
|
||||||
const UserSettingsBuilder({
|
const UserSettingsBuilder({
|
||||||
@@ -17,14 +17,11 @@ class UserSettingsBuilder extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<Box<UserAppSettings>>(
|
return ValueListenableBuilder<Box<UserSettings>>(
|
||||||
valueListenable:
|
valueListenable: Hive.box<UserSettings>(HiveBoxes.userSettings).listenable(),
|
||||||
Hive.box<UserAppSettings>(HiveBoxes.userSettings).listenable(),
|
|
||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
final currentUser =
|
final currentUser =
|
||||||
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
||||||
.get(HiveBoxSingleValueKey.value)
|
|
||||||
?.currentLoggedInUser;
|
|
||||||
if (currentUser != null) {
|
if (currentUser != null) {
|
||||||
final settings = value.get(currentUser);
|
final settings = value.get(currentUser);
|
||||||
return builder(context, settings);
|
return builder(context, settings);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"biometricAuthentication": "Biometric authentication",
|
"biometricAuthentication": "Biometric authentication",
|
||||||
"@biometricAuthentication": {},
|
"@biometricAuthentication": {},
|
||||||
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
|
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
|
||||||
"@authenticateToToggleBiometricAuthentication": {
|
"@authenticateToToggleBiometricAuthentication": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"mode": {}
|
"mode": {}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"developedBy": "Developed by {name}",
|
"developedBy": "Stworzone przez {name}",
|
||||||
"@developedBy": {
|
"@developedBy": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {}
|
"name": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"addAnotherAccount": "Add another account",
|
"addAnotherAccount": "Dodaj kolejne konto",
|
||||||
"@addAnotherAccount": {},
|
"@addAnotherAccount": {},
|
||||||
"account": "Account",
|
"account": "Konto",
|
||||||
"@account": {},
|
"@account": {},
|
||||||
"addCorrespondent": "New Correspondent",
|
"addCorrespondent": "New Correspondent",
|
||||||
"@addCorrespondent": {
|
"@addCorrespondent": {
|
||||||
@@ -35,11 +35,11 @@
|
|||||||
"name": {}
|
"name": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Rozłącz się",
|
||||||
"@disconnect": {
|
"@disconnect": {
|
||||||
"description": "Logout button label"
|
"description": "Logout button label"
|
||||||
},
|
},
|
||||||
"reportABug": "Report a Bug",
|
"reportABug": "Zgłoś błąd",
|
||||||
"@reportABug": {},
|
"@reportABug": {},
|
||||||
"settings": "Ustawienia",
|
"settings": "Ustawienia",
|
||||||
"@settings": {},
|
"@settings": {},
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"biometricAuthentication": "Biometric authentication",
|
"biometricAuthentication": "Biometric authentication",
|
||||||
"@biometricAuthentication": {},
|
"@biometricAuthentication": {},
|
||||||
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
|
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
|
||||||
"@authenticateToToggleBiometricAuthentication": {
|
"@authenticateToToggleBiometricAuthentication": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"mode": {}
|
"mode": {}
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"@startTyping": {},
|
"@startTyping": {},
|
||||||
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
|
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
|
||||||
"@doYouReallyWantToDeleteThisView": {},
|
"@doYouReallyWantToDeleteThisView": {},
|
||||||
"deleteView": "Delete view ",
|
"deleteView": "Usuń widok ",
|
||||||
"@deleteView": {},
|
"@deleteView": {},
|
||||||
"addedAt": "Added at",
|
"addedAt": "Added at",
|
||||||
"@addedAt": {},
|
"@addedAt": {},
|
||||||
@@ -141,13 +141,13 @@
|
|||||||
},
|
},
|
||||||
"documentSuccessfullyDownloaded": "Document successfully downloaded.",
|
"documentSuccessfullyDownloaded": "Document successfully downloaded.",
|
||||||
"@documentSuccessfullyDownloaded": {},
|
"@documentSuccessfullyDownloaded": {},
|
||||||
"suggestions": "Suggestions: ",
|
"suggestions": "Sugestie: ",
|
||||||
"@suggestions": {},
|
"@suggestions": {},
|
||||||
"editDocument": "Edytuj Dokument",
|
"editDocument": "Edytuj Dokument",
|
||||||
"@editDocument": {},
|
"@editDocument": {},
|
||||||
"advanced": "Advanced",
|
"advanced": "Zaawansowane",
|
||||||
"@advanced": {},
|
"@advanced": {},
|
||||||
"apply": "Apply",
|
"apply": "Zastosuj",
|
||||||
"@apply": {},
|
"@apply": {},
|
||||||
"extended": "Extended",
|
"extended": "Extended",
|
||||||
"@extended": {},
|
"@extended": {},
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
"@titleAndContent": {},
|
"@titleAndContent": {},
|
||||||
"title": "Tytuł",
|
"title": "Tytuł",
|
||||||
"@title": {},
|
"@title": {},
|
||||||
"reset": "Reset",
|
"reset": "Zresetuj",
|
||||||
"@reset": {},
|
"@reset": {},
|
||||||
"filterDocuments": "Filter Documents",
|
"filterDocuments": "Filter Documents",
|
||||||
"@filterDocuments": {
|
"@filterDocuments": {
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
},
|
},
|
||||||
"originalMD5Checksum": "Original MD5-Checksum",
|
"originalMD5Checksum": "Original MD5-Checksum",
|
||||||
"@originalMD5Checksum": {},
|
"@originalMD5Checksum": {},
|
||||||
"mediaFilename": "Media Filename",
|
"mediaFilename": "Nazwa pliku",
|
||||||
"@mediaFilename": {},
|
"@mediaFilename": {},
|
||||||
"originalFileSize": "Original File Size",
|
"originalFileSize": "Original File Size",
|
||||||
"@originalFileSize": {},
|
"@originalFileSize": {},
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
"@or": {
|
"@or": {
|
||||||
"description": "Used on the scanner page between both main actions when no scans have been captured."
|
"description": "Used on the scanner page between both main actions when no scans have been captured."
|
||||||
},
|
},
|
||||||
"deleteAllScans": "Delete all scans",
|
"deleteAllScans": "Usuń wszystkie skany",
|
||||||
"@deleteAllScans": {},
|
"@deleteAllScans": {},
|
||||||
"uploadADocumentFromThisDevice": "Upload a document from this device",
|
"uploadADocumentFromThisDevice": "Upload a document from this device",
|
||||||
"@uploadADocumentFromThisDevice": {
|
"@uploadADocumentFromThisDevice": {
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
},
|
},
|
||||||
"searchDocuments": "Search documents",
|
"searchDocuments": "Search documents",
|
||||||
"@searchDocuments": {},
|
"@searchDocuments": {},
|
||||||
"resetFilter": "Reset filter",
|
"resetFilter": "Zresetuj filtr",
|
||||||
"@resetFilter": {},
|
"@resetFilter": {},
|
||||||
"lastMonth": "Last Month",
|
"lastMonth": "Last Month",
|
||||||
"@lastMonth": {},
|
"@lastMonth": {},
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
"@oops": {},
|
"@oops": {},
|
||||||
"newDocumentAvailable": "New document available!",
|
"newDocumentAvailable": "New document available!",
|
||||||
"@newDocumentAvailable": {},
|
"@newDocumentAvailable": {},
|
||||||
"orderBy": "Order By",
|
"orderBy": "Sortuj według",
|
||||||
"@orderBy": {},
|
"@orderBy": {},
|
||||||
"thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?",
|
"thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?",
|
||||||
"@thisActionIsIrreversibleDoYouWishToProceedAnyway": {},
|
"@thisActionIsIrreversibleDoYouWishToProceedAnyway": {},
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
"@fileName": {},
|
"@fileName": {},
|
||||||
"synchronizeTitleAndFilename": "Synchronize title and filename",
|
"synchronizeTitleAndFilename": "Synchronize title and filename",
|
||||||
"@synchronizeTitleAndFilename": {},
|
"@synchronizeTitleAndFilename": {},
|
||||||
"reload": "Reload",
|
"reload": "Odśwież",
|
||||||
"@reload": {},
|
"@reload": {},
|
||||||
"documentSuccessfullyUploadedProcessing": "Dokument pomyślnie przesłany, przetwarzam...",
|
"documentSuccessfullyUploadedProcessing": "Dokument pomyślnie przesłany, przetwarzam...",
|
||||||
"@documentSuccessfullyUploadedProcessing": {},
|
"@documentSuccessfullyUploadedProcessing": {},
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"biometricAuthentication": "Biometric authentication",
|
"biometricAuthentication": "Biometric authentication",
|
||||||
"@biometricAuthentication": {},
|
"@biometricAuthentication": {},
|
||||||
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
|
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
|
||||||
"@authenticateToToggleBiometricAuthentication": {
|
"@authenticateToToggleBiometricAuthentication": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"mode": {}
|
"mode": {}
|
||||||
|
|||||||
182
lib/main.dart
182
lib/main.dart
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
|
||||||
@@ -9,14 +10,13 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:intl/intl_standalone.dart';
|
import 'package:intl/intl_standalone.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
@@ -28,32 +28,33 @@ import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/core/service/dio_file_service.dart';
|
import 'package:paperless_mobile/core/service/dio_file_service.dart';
|
||||||
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/home_page.dart';
|
import 'package:paperless_mobile/features/home/view/home_page.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
|
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/user_account.dart';
|
||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/model/user_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/routes/document_details_route.dart';
|
import 'package:paperless_mobile/routes/document_details_route.dart';
|
||||||
import 'package:paperless_mobile/theme.dart';
|
import 'package:paperless_mobile/theme.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
|
||||||
|
|
||||||
String get defaultPreferredLocaleSubtag {
|
String get defaultPreferredLocaleSubtag {
|
||||||
String preferredLocale = Platform.localeName.split("_").first;
|
String preferredLocale = Platform.localeName.split("_").first;
|
||||||
if (!S.supportedLocales
|
if (!S.supportedLocales.any((locale) => locale.languageCode == preferredLocale)) {
|
||||||
.any((locale) => locale.languageCode == preferredLocale)) {
|
|
||||||
preferredLocale = 'en';
|
preferredLocale = 'en';
|
||||||
}
|
}
|
||||||
return preferredLocale;
|
return preferredLocale;
|
||||||
@@ -61,23 +62,25 @@ String get defaultPreferredLocaleSubtag {
|
|||||||
|
|
||||||
Future<void> _initHive() async {
|
Future<void> _initHive() async {
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
|
//TODO: REMOVE!
|
||||||
|
// await getApplicationDocumentsDirectory().then((value) => value.delete(recursive: true));
|
||||||
|
|
||||||
registerHiveAdapters();
|
registerHiveAdapters();
|
||||||
final globalSettingsBox =
|
await Hive.openBox<UserAccount>(HiveBoxes.userAccount);
|
||||||
await Hive.openBox<GlobalAppSettings>(HiveBoxes.globalSettings);
|
await Hive.openBox<UserSettings>(HiveBoxes.userSettings);
|
||||||
if (!globalSettingsBox.containsKey(HiveBoxSingleValueKey.value)) {
|
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
||||||
await globalSettingsBox.put(
|
|
||||||
HiveBoxSingleValueKey.value,
|
if (!globalSettingsBox.hasValue) {
|
||||||
GlobalAppSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag),
|
await globalSettingsBox
|
||||||
);
|
.setValue(GlobalSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await _initHive();
|
await _initHive();
|
||||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final globalSettingsBox = Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||||
final globalSettings = Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
|
final globalSettings = globalSettingsBox.getValue()!;
|
||||||
.get(HiveBoxSingleValueKey.value)!;
|
|
||||||
|
|
||||||
await findSystemLocale();
|
await findSystemLocale();
|
||||||
packageInfo = await PackageInfo.fromPlatform();
|
packageInfo = await PackageInfo.fromPlatform();
|
||||||
@@ -95,9 +98,8 @@ void main() async {
|
|||||||
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
|
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
|
||||||
final localAuthService = LocalAuthenticationService(localAuthentication);
|
final localAuthService = LocalAuthenticationService(localAuthentication);
|
||||||
|
|
||||||
final hiveDir = await getApplicationDocumentsDirectory();
|
|
||||||
HydratedBloc.storage = await HydratedStorage.build(
|
HydratedBloc.storage = await HydratedStorage.build(
|
||||||
storageDirectory: hiveDir,
|
storageDirectory: await getApplicationDocumentsDirectory(),
|
||||||
);
|
);
|
||||||
|
|
||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
@@ -136,34 +138,27 @@ void main() async {
|
|||||||
localAuthService,
|
localAuthService,
|
||||||
authApi,
|
authApi,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
|
labelRepository,
|
||||||
|
savedViewRepository,
|
||||||
|
statsApi,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (globalSettings.currentLoggedInUser != null) {
|
if (globalSettings.currentLoggedInUser != null) {
|
||||||
await authCubit
|
await authCubit.restoreSessionState();
|
||||||
.restoreSessionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authCubit.state.isAuthenticated) {
|
|
||||||
final auth = authCubit.state.authentication!;
|
|
||||||
sessionManager.updateSettings(
|
|
||||||
baseUrl: auth.serverUrl,
|
|
||||||
authToken: auth.token,
|
|
||||||
clientCertificate: auth.clientCertificate,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final localNotificationService = LocalNotificationService();
|
final localNotificationService = LocalNotificationService();
|
||||||
await localNotificationService.initialize();
|
await localNotificationService.initialize();
|
||||||
|
|
||||||
//Update language header in interceptor on language change.
|
//Update language header in interceptor on language change.
|
||||||
globalSettings.addListener(
|
globalSettingsBox.listenable().addListener(() {
|
||||||
() => languageHeaderInterceptor.preferredLocaleSubtag =
|
languageHeaderInterceptor.preferredLocaleSubtag = globalSettings.preferredLocaleSubtag;
|
||||||
globalSettings.preferredLocaleSubtag,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
Provider<LocalAuthenticationService>.value(value: localAuthService),
|
||||||
Provider<PaperlessAuthenticationApi>.value(value: authApi),
|
Provider<PaperlessAuthenticationApi>.value(value: authApi),
|
||||||
Provider<PaperlessDocumentsApi>.value(value: documentsApi),
|
Provider<PaperlessDocumentsApi>.value(value: documentsApi),
|
||||||
Provider<PaperlessLabelsApi>.value(value: labelsApi),
|
Provider<PaperlessLabelsApi>.value(value: labelsApi),
|
||||||
@@ -181,8 +176,7 @@ void main() async {
|
|||||||
Provider<ConnectivityStatusService>.value(
|
Provider<ConnectivityStatusService>.value(
|
||||||
value: connectivityStatusService,
|
value: connectivityStatusService,
|
||||||
),
|
),
|
||||||
Provider<LocalNotificationService>.value(
|
Provider<LocalNotificationService>.value(value: localNotificationService),
|
||||||
value: localNotificationService),
|
|
||||||
Provider.value(value: DocumentChangedNotifier()),
|
Provider.value(value: DocumentChangedNotifier()),
|
||||||
],
|
],
|
||||||
child: MultiRepositoryProvider(
|
child: MultiRepositoryProvider(
|
||||||
@@ -212,22 +206,13 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PaperlessMobileEntrypoint> createState() =>
|
State<PaperlessMobileEntrypoint> createState() => _PaperlessMobileEntrypointState();
|
||||||
_PaperlessMobileEntrypointState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocProvider(
|
return GlobalSettingsBuilder(
|
||||||
providers: [
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) => PaperlessServerInformationCubit(
|
|
||||||
context.read<PaperlessServerStatsApi>(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: GlobalSettingsBuilder(
|
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
@@ -256,15 +241,13 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
],
|
],
|
||||||
routes: {
|
routes: {
|
||||||
DocumentDetailsRoute.routeName: (context) =>
|
DocumentDetailsRoute.routeName: (context) => const DocumentDetailsRoute(),
|
||||||
const DocumentDetailsRoute(),
|
|
||||||
},
|
},
|
||||||
home: const AuthenticationWrapper(),
|
home: const AuthenticationWrapper(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,25 +262,23 @@ class AuthenticationWrapper extends StatefulWidget {
|
|||||||
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Temporary Fix: Can be removed if the flutter engine implements the fix itself
|
|
||||||
// Activate the highest supported refresh rate on the device
|
// Activate the highest supported refresh rate on the device
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
_setOptimalDisplayMode();
|
_setOptimalDisplayMode();
|
||||||
}
|
}
|
||||||
initializeDateFormatting();
|
initializeDateFormatting();
|
||||||
// For sharing files coming from outside the app while the app is still opened
|
// For sharing files coming from outside the app while the app is still opened
|
||||||
ReceiveSharingIntent.getMediaStream()
|
ReceiveSharingIntent.getMediaStream().listen(ShareIntentQueue.instance.addAll);
|
||||||
.listen(ShareIntentQueue.instance.addAll);
|
|
||||||
// For sharing files coming from outside the app while the app is closed
|
// For sharing files coming from outside the app while the app is closed
|
||||||
ReceiveSharingIntent.getInitialMedia()
|
ReceiveSharingIntent.getInitialMedia().then(ShareIntentQueue.instance.addAll);
|
||||||
.then(ShareIntentQueue.instance.addAll);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setOptimalDisplayMode() async {
|
Future<void> _setOptimalDisplayMode() async {
|
||||||
@@ -309,8 +290,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
|
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
|
||||||
|
|
||||||
final DisplayMode mostOptimalMode =
|
final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active;
|
||||||
sameResolution.isNotEmpty ? sameResolution.first : active;
|
|
||||||
debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
|
debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}');
|
||||||
|
|
||||||
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
|
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
|
||||||
@@ -318,36 +298,76 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocConsumer<AuthenticationCubit, AuthenticationState>(
|
return BlocBuilder<AuthenticationCubit, AuthenticationState>(
|
||||||
listener: (context, authState) {
|
builder: (context, authentication) {
|
||||||
final bool showIntroSlider =
|
if (authentication.isAuthenticated) {
|
||||||
authState.isAuthenticated && !authState.wasLoginStored;
|
return MultiBlocProvider(
|
||||||
if (showIntroSlider) {
|
// This key will cause the subtree to be remounted, which will again
|
||||||
|
// call the provider's create callback! Without this key, the blocs
|
||||||
|
// would not be recreated on account switch!
|
||||||
|
key: ValueKey(authentication.userId),
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => TaskStatusCubit(
|
||||||
|
context.read<PaperlessTasksApi>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider<PaperlessServerInformationCubit>(
|
||||||
|
create: (context) => PaperlessServerInformationCubit(
|
||||||
|
context.read<PaperlessServerStatsApi>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const HomePage(),
|
||||||
|
);
|
||||||
|
} else if (authentication.showBiometricAuthenticationScreen) {
|
||||||
|
return const VerifyIdentityPage();
|
||||||
|
}
|
||||||
|
return LoginPage(
|
||||||
|
submitText: S.of(context)!.signIn,
|
||||||
|
onSubmit: _onLogin,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLogin(
|
||||||
|
BuildContext context,
|
||||||
|
String username,
|
||||||
|
String password,
|
||||||
|
String serverUrl,
|
||||||
|
ClientCertificate? clientCertificate,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await context.read<AuthenticationCubit>().login(
|
||||||
|
credentials: LoginFormCredentials(username: username, password: password),
|
||||||
|
serverUrl: serverUrl,
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
);
|
||||||
|
// Show onboarding after first login!
|
||||||
|
final globalSettings = GlobalSettings.boxedValue;
|
||||||
|
if (globalSettings.showOnboarding) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const ApplicationIntroSlideshow(),
|
builder: (context) => const ApplicationIntroSlideshow(),
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
);
|
).then((value) {
|
||||||
|
globalSettings.showOnboarding = false;
|
||||||
|
globalSettings.save();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
builder: (context, authentication) {
|
showErrorMessage(context, error, stackTrace);
|
||||||
if (authentication.isAuthenticated &&
|
} on PaperlessValidationErrors catch (error, stackTrace) {
|
||||||
(authentication.wasLocalAuthenticationSuccessful ?? true)) {
|
if (error.hasFieldUnspecificError) {
|
||||||
return BlocProvider(
|
showLocalizedError(context, error.fieldUnspecificError!);
|
||||||
create: (context) =>
|
|
||||||
TaskStatusCubit(context.read<PaperlessTasksApi>()),
|
|
||||||
child: const HomePage(),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
if (authentication.wasLoginStored &&
|
showGenericError(context, error.values.first, stackTrace);
|
||||||
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
|
|
||||||
return const VerifyIdentityPage();
|
|
||||||
}
|
}
|
||||||
return const LoginPage();
|
} catch (unknownError, stackTrace) {
|
||||||
|
showGenericError(context, unknownError.toString(), stackTrace);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_information_model.dart';
|
||||||
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
|
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
|
||||||
|
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
|
||||||
|
|
||||||
abstract class PaperlessServerStatsApi {
|
abstract class PaperlessServerStatsApi {
|
||||||
Future<PaperlessServerInformationModel> getServerInformation();
|
Future<PaperlessServerInformationModel> getServerInformation();
|
||||||
Future<PaperlessServerStatisticsModel> getServerStatistics();
|
Future<PaperlessServerStatisticsModel> getServerStatistics();
|
||||||
|
Future<PaperlessUiSettingsModel> getUiSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:http/http.dart';
|
|||||||
import 'package:paperless_api/src/models/paperless_server_exception.dart';
|
import 'package:paperless_api/src/models/paperless_server_exception.dart';
|
||||||
import 'package:paperless_api/src/models/paperless_server_information_model.dart';
|
import 'package:paperless_api/src/models/paperless_server_information_model.dart';
|
||||||
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
|
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
|
||||||
|
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
|
||||||
|
|
||||||
import 'paperless_server_stats_api.dart';
|
import 'paperless_server_stats_api.dart';
|
||||||
|
|
||||||
@@ -21,15 +22,12 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
|
|||||||
@override
|
@override
|
||||||
Future<PaperlessServerInformationModel> getServerInformation() async {
|
Future<PaperlessServerInformationModel> getServerInformation() async {
|
||||||
final response = await client.get("/api/ui_settings/");
|
final response = await client.get("/api/ui_settings/");
|
||||||
final version = response
|
final version =
|
||||||
.headers[PaperlessServerInformationModel.versionHeader]?.first ??
|
response.headers[PaperlessServerInformationModel.versionHeader]?.first ?? 'unknown';
|
||||||
'unknown';
|
final apiVersion = int.tryParse(
|
||||||
final apiVersion = int.tryParse(response
|
response.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ?? '1');
|
||||||
.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ??
|
|
||||||
'1');
|
|
||||||
final String username = response.data['username'];
|
final String username = response.data['username'];
|
||||||
final String host = response
|
final String host = response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
|
||||||
.headers[PaperlessServerInformationModel.hostHeader]?.first ??
|
|
||||||
response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
|
response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
|
||||||
('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}');
|
('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}');
|
||||||
return PaperlessServerInformationModel(
|
return PaperlessServerInformationModel(
|
||||||
@@ -48,4 +46,13 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
|
|||||||
}
|
}
|
||||||
throw const PaperlessServerException.unknown();
|
throw const PaperlessServerException.unknown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaperlessUiSettingsModel> getUiSettings() async {
|
||||||
|
final response = await client.get("/api/ui_settings/");
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return PaperlessUiSettingsModel.fromJson(response.data);
|
||||||
|
}
|
||||||
|
throw const PaperlessServerException.unknown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user