feat: Split settings, encrypted user credentials, preparations for multi user support

This commit is contained in:
Anton Stubenbord
2023-04-14 01:34:34 +02:00
parent c6264f6fc0
commit 1334f546ee
27 changed files with 520 additions and 173 deletions

View File

@@ -69,10 +69,7 @@ class AppDrawer extends StatelessWidget {
),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: context.read<ApplicationSettingsCubit>(),
child: const SettingsPage(),
),
builder: (context) => const SettingsPage(),
),
),
),

View File

@@ -12,10 +12,13 @@ class ConfirmBulkModifyLabelDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Text(S.of(context)!.confirmAction),
content: RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
text: content,
children: [
const TextSpan(text: "\n\n"),

View File

@@ -18,10 +18,13 @@ class ConfirmBulkModifyTagsDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Text(S.of(context)!.confirmAction),
content: RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
text: _buildText(context),
children: [
const TextSpan(text: "\n\n"),

View File

@@ -5,6 +5,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -69,10 +70,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
setState(() => _isDownloadPending = true);
await context.read<DocumentDetailsCubit>().downloadDocument(
downloadOriginal: downloadOriginal,
locale: context
.read<ApplicationSettingsCubit>()
.state
.preferredLocaleSubtag,
locale: context.read<GlobalAppSettings>().preferredLocaleSubtag,
);
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
} on PaperlessServerException catch (error, stackTrace) {

View File

@@ -1,17 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
class DocumentListItem extends DocumentItem {
static const _a4AspectRatio = 1 / 1.4142;
DocumentListItem({
const DocumentListItem({
super.key,
required super.document,
required super.isSelected,
@@ -28,9 +25,7 @@ class DocumentListItem extends DocumentItem {
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
}) {
print(tags.keys.join(", "));
}
});
@override
Widget build(BuildContext context) {

View File

@@ -5,7 +5,10 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
@@ -22,42 +25,48 @@ class VerifyIdentityPage extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.background,
title: Text(S.of(context)!.verifyYourIdentity),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16),
const Icon(
Icons.fingerprint,
size: 96,
),
Wrap(
alignment: WrapAlignment.spaceBetween,
runAlignment: WrapAlignment.spaceBetween,
runSpacing: 8,
spacing: 8,
body: UserSettingsBuilder(
builder: (context, settings) {
if (settings == null) {
return const SizedBox.shrink();
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => _logout(context),
child: Text(
S.of(context)!.disconnect,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
Text(S
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16),
const Icon(
Icons.fingerprint,
size: 96,
),
Wrap(
alignment: WrapAlignment.spaceBetween,
runAlignment: WrapAlignment.spaceBetween,
runSpacing: 8,
spacing: 8,
children: [
TextButton(
onPressed: () => _logout(context),
child: Text(
S.of(context)!.disconnect,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
),
),
ElevatedButton(
onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(context
.read<ApplicationSettingsCubit>()
.state
.isLocalAuthenticationEnabled),
child: Text(S.of(context)!.verifyIdentity),
),
ElevatedButton(
onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(),
child: Text(S.of(context)!.verifyIdentity),
),
],
).padded(16),
],
).padded(16),
],
);
},
),
),
);

View File

@@ -1,17 +1,22 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
part 'authentication_state.dart';
part 'authentication_cubit.g.dart';
class AuthenticationCubit extends Cubit<AuthenticationState>
with HydratedMixin<AuthenticationState> {
class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi;
final SessionManager _dioWrapper;
@@ -42,15 +47,31 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
clientCertificate: clientCertificate,
authToken: token,
);
final authInfo = AuthenticationInformation(
username: credentials.username!,
serverUrl: serverUrl,
clientCertificate: clientCertificate,
token: token,
);
final userId = "${credentials.username}@$serverUrl";
// Mark logged in user as currently active user.
final globalSettings = GlobalAppSettings.boxedValue;
globalSettings.currentLoggedInUser = userId;
await globalSettings.save();
// Save credentials in encrypted box
final encryptedBox = await _openEncryptedBox();
await encryptedBox.put(
userId,
authInfo,
);
encryptedBox.close();
emit(
AuthenticationState(
wasLoginStored: false,
authentication: AuthenticationInformation(
serverUrl: serverUrl,
clientCertificate: clientCertificate,
token: token,
),
authentication: authInfo,
),
);
}
@@ -58,24 +79,27 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
///
/// Performs a conditional hydration based on the local authentication success.
///
Future<void> restoreSessionState(bool promptForLocalAuthentication) async {
final json = HydratedBloc.storage.read(storageToken);
if (json == null) {
Future<void> restoreSessionState() async {
final globalSettings = GlobalAppSettings.boxedValue;
if (globalSettings.currentLoggedInUser == null) {
// If there is nothing to restore, we can quit here.
return;
}
if (promptForLocalAuthentication) {
final userSettings = Hive.box<UserAppSettings>(HiveBoxes.userSettings)
.get(globalSettings.currentLoggedInUser!);
if (userSettings!.isBiometricAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in");
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (localAuthSuccess) {
hydrate();
if (state.isAuthenticated) {
final authentication = await _readAuthenticationFromEncryptedBox(
globalSettings.currentLoggedInUser!);
if (authentication != null) {
_dioWrapper.updateSettings(
clientCertificate: state.authentication!.clientCertificate,
authToken: state.authentication!.token,
baseUrl: state.authentication!.serverUrl,
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: authentication.serverUrl,
);
return emit(
AuthenticationState(
@@ -86,44 +110,62 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
);
}
} else {
hydrate();
return emit(
AuthenticationState(
wasLoginStored: true,
wasLocalAuthenticationSuccessful: false,
authentication: state.authentication,
authentication: null,
),
);
}
} else {
hydrate();
if (state.isAuthenticated) {
final authentication = await _readAuthenticationFromEncryptedBox(
globalSettings.currentLoggedInUser!);
if (authentication != null) {
_dioWrapper.updateSettings(
clientCertificate: state.authentication!.clientCertificate,
authToken: state.authentication!.token,
baseUrl: state.authentication!.serverUrl,
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: authentication.serverUrl,
);
final authState = AuthenticationState(
authentication: state.authentication!,
wasLoginStored: true,
emit(
AuthenticationState(
authentication: authentication,
wasLoginStored: true,
),
);
return emit(authState);
} else {
return emit(AuthenticationState.initial);
}
}
}
Future<AuthenticationInformation?> _readAuthenticationFromEncryptedBox(
String userId) {
return _openEncryptedBox().then((box) => box.get(userId));
}
Future<Box<AuthenticationInformation?>> _openEncryptedBox() async {
const secureStorage = FlutterSecureStorage();
final encryptionKeyString = await secureStorage.read(key: 'key');
if (encryptionKeyString == null) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'key',
value: base64UrlEncode(key),
);
}
final key = await secureStorage.read(key: 'key');
final encryptionKeyUint8List = base64Url.decode(key!);
return await Hive.openBox<AuthenticationInformation>(
HiveBoxes.vault,
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
);
}
Future<void> logout() async {
await clear();
await Hive.box<AuthenticationInformation>(HiveBoxes.authentication).clear();
_dioWrapper.resetSettings();
emit(AuthenticationState.initial);
}
@override
AuthenticationState? fromJson(Map<String, dynamic> json) =>
AuthenticationState.fromJson(json);
@override
Map<String, dynamic>? toJson(AuthenticationState state) => state.toJson();
}

View File

@@ -12,6 +12,7 @@ class AuthenticationState {
);
bool get isAuthenticated => authentication != null;
AuthenticationState({
required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
@@ -31,9 +32,4 @@ class AuthenticationState {
this.wasLocalAuthenticationSuccessful,
);
}
factory AuthenticationState.fromJson(Map<String, dynamic> json) =>
_$AuthenticationStateFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticationStateToJson(this);
}

View File

@@ -1,40 +1,32 @@
import 'package:hive/hive.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
part 'authentication_information.g.dart';
@JsonSerializable()
@HiveType(typeId: HiveTypeIds.authentication)
class AuthenticationInformation {
final String? token;
final String serverUrl;
final ClientCertificate? clientCertificate;
@HiveField(0)
String? token;
@HiveField(1)
String serverUrl;
@HiveField(2)
ClientCertificate? clientCertificate;
@HiveField(3)
String username;
AuthenticationInformation({
this.token,
required this.username,
required this.serverUrl,
this.token,
this.clientCertificate,
});
bool get isValid {
return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false);
}
AuthenticationInformation copyWith({
String? token,
String? serverUrl,
ClientCertificate? clientCertificate,
bool removeClientCertificate = false,
}) {
return AuthenticationInformation(
token: token ?? this.token,
serverUrl: serverUrl ?? this.serverUrl,
clientCertificate: clientCertificate ??
(removeClientCertificate ? null : this.clientCertificate),
);
}
factory AuthenticationInformation.fromJson(Map<String, dynamic> json) =>
_$AuthenticationInformationFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticationInformationToJson(this);
}

View File

@@ -1,13 +1,20 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/type/types.dart';
part 'client_certificate.g.dart';
@HiveType(typeId: HiveTypeIds.clientCertificate)
class ClientCertificate {
static const bytesKey = 'bytes';
static const passphraseKey = 'passphrase';
@HiveField(0)
final Uint8List bytes;
@HiveField(1)
final String? passphrase;
ClientCertificate({required this.bytes, this.passphrase});

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
part 'global_app_settings.g.dart';
@HiveType(typeId: HiveTypeIds.globalSettings)
class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
@HiveField(0)
String preferredLocaleSubtag;
@HiveField(1)
ThemeMode preferredThemeMode;
@HiveField(2)
ColorSchemeOption preferredColorSchemeOption;
@HiveField(3)
bool showOnboarding;
@HiveField(4)
String? currentLoggedInUser;
GlobalAppSettings({
required this.preferredLocaleSubtag,
this.preferredThemeMode = ThemeMode.system,
this.preferredColorSchemeOption = ColorSchemeOption.classic,
this.showOnboarding = true,
this.currentLoggedInUser,
});
static GlobalAppSettings get boxedValue =>
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
.get(HiveBoxSingleValueKey.value)!;
}

View File

@@ -1,4 +1,12 @@
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
part 'color_scheme_option.g.dart';
@HiveType(typeId: HiveTypeIds.colorSchemeOption)
enum ColorSchemeOption {
@HiveField(0)
classic,
@HiveField(1)
dynamic;
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
part 'user_app_settings.g.dart';
@HiveType(typeId: HiveTypeIds.userSettings)
class UserAppSettings with HiveObjectMixin {
@HiveField(0)
bool isBiometricAuthenticationEnabled;
UserAppSettings({
this.isBiometricAuthenticationEnabled = false,
});
}

View File

@@ -9,6 +9,7 @@ import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -81,7 +82,7 @@ class AccountSettingsDialog extends StatelessWidget {
Future<void> _onLogout(BuildContext context) async {
try {
await context.read<AuthenticationCubit>().logout();
await context.read<ApplicationSettingsCubit>().clear();
await context.read<GlobalAppSettings>();
await context.read<LabelRepository>().clear();
await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear();

View File

@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@@ -68,10 +70,7 @@ class SettingsPage extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: context.read<ApplicationSettingsCubit>(),
child: page,
),
builder: (context) => page,
maintainState: true,
),
);

View File

@@ -1,17 +1,27 @@
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/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class BiometricAuthenticationSetting extends StatelessWidget {
const BiometricAuthenticationSetting({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
return UserSettingsBuilder(
builder: (context, settings) {
if (settings == null) {
return const SizedBox.shrink();
}
return SwitchListTile(
value: settings.isLocalAuthenticationEnabled,
value: settings.isBiometricAuthenticationEnabled,
title: Text(S.of(context)!.biometricAuthentication),
subtitle: Text(S.of(context)!.authenticateOnAppStart),
onChanged: (val) async {
@@ -19,12 +29,14 @@ class BiometricAuthenticationSetting extends StatelessWidget {
S.of(context)!.authenticateToToggleBiometricAuthentication(
val ? 'enable' : 'disable',
);
await context
.read<ApplicationSettingsCubit>()
.setIsBiometricAuthenticationEnabled(
val,
localizedReason: localizedReason,
);
final isAuthenticated = await context
.read<LocalAuthenticationService>()
.authenticateLocalUser(localizedReason);
if (isAuthenticated) {
settings.isBiometricAuthenticationEnabled = val;
settings.save();
}
},
);
},

View File

@@ -2,11 +2,15 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -15,7 +19,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
return GlobalSettingsBuilder(
builder: (context, settings) {
return ListTile(
title: Text(S.of(context)!.colors),
@@ -25,7 +29,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
settings.preferredColorSchemeOption,
),
),
onTap: () => showDialog(
onTap: () => showDialog<ColorSchemeOption>(
context: context,
builder: (_) => RadioSettingsDialog<ColorSchemeOption>(
titleText: S.of(context)!.colors,
@@ -50,17 +54,13 @@ class ColorSchemeOptionSetting extends StatelessWidget {
hintIcon: Icons.warning_amber,
)
: null,
initialValue: context
.read<ApplicationSettingsCubit>()
.state
.preferredColorSchemeOption,
initialValue: settings.preferredColorSchemeOption,
),
).then(
(value) {
if (value != null) {
context
.read<ApplicationSettingsCubit>()
.setColorSchemeOption(value);
settings.preferredColorSchemeOption = value;
settings.save();
}
},
),

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
class GlobalSettingsBuilder extends StatelessWidget {
final Widget Function(BuildContext context, GlobalAppSettings settings)
builder;
const GlobalSettingsBuilder({super.key, required this.builder});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable:
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings).listenable(),
builder: (context, value, _) {
final settings = value.get(HiveBoxSingleValueKey.value)!;
return builder(context, settings);
},
);
}
}

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
class LanguageSelectionSetting extends StatefulWidget {
const LanguageSelectionSetting({super.key});
@@ -24,7 +27,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
@override
Widget build(BuildContext context) {
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
return GlobalSettingsBuilder(
builder: (context, settings) {
return ListTile(
title: Text(S.of(context)!.language),
@@ -62,14 +65,12 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
label: _languageOptions['pl']! + "*",
)
],
initialValue: context
.read<ApplicationSettingsCubit>()
.state
.preferredLocaleSubtag,
initialValue: settings.preferredLocaleSubtag,
),
).then((value) {
if (value != null) {
context.read<ApplicationSettingsCubit>().setLocale(value);
settings.preferredLocaleSubtag = value;
settings.save();
}
}),
);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -9,7 +10,7 @@ class ThemeModeSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
return GlobalSettingsBuilder(
builder: (context, settings) {
return ListTile(
title: Text(S.of(context)!.appearance),
@@ -19,10 +20,7 @@ class ThemeModeSetting extends StatelessWidget {
context: context,
builder: (_) => RadioSettingsDialog<ThemeMode>(
titleText: S.of(context)!.appearance,
initialValue: context
.read<ApplicationSettingsCubit>()
.state
.preferredThemeMode,
initialValue: settings.preferredThemeMode,
options: [
RadioOption(
value: ThemeMode.system,
@@ -40,7 +38,8 @@ class ThemeModeSetting extends StatelessWidget {
),
).then((value) {
if (value != null) {
context.read<ApplicationSettingsCubit>().setThemeMode(value);
settings.preferredThemeMode = value;
settings.save();
}
}),
);

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
class UserSettingsBuilder extends StatelessWidget {
final Widget Function(
BuildContext context,
UserAppSettings? settings,
) builder;
const UserSettingsBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Box<UserAppSettings>>(
valueListenable:
Hive.box<UserAppSettings>(HiveBoxes.userSettings).listenable(),
builder: (context, value, _) {
final currentUser =
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
.get(HiveBoxSingleValueKey.value)
?.currentLoggedInUser;
if (currentUser != null) {
final settings = value.get(currentUser);
return builder(context, settings);
} else {
return builder(context, null);
}
},
);
}
}