Finish recovery key screen

pull/90/head
Inex Code 2022-05-31 02:06:08 +03:00
parent 1db8e9556e
commit 8ec3b8c3e3
19 changed files with 285 additions and 213 deletions

View File

@ -291,7 +291,7 @@
"method_select_other_device": "I have access on another device",
"method_select_recovery_key": "I have a recovery key",
"method_select_nothing": "I don't have any of that",
"method_device_description": "Open the application on another device, then go to the device page. Press \"Add device\" to receive your token.",
"method_device_description": "Open the application on another device, then go to the devices page. Press \"Add device\" to receive your token.",
"method_device_button": "I have received my token",
"method_device_input_description": "Enter your authorization token",
"method_device_input_placeholder": "Token",
@ -342,7 +342,8 @@
"key_replace_button": "Generate new key",
"key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:",
"key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.",
"key_receiving_done": "Done!"
"key_receiving_done": "Done!",
"generation_error": "Couldn't generate a recovery key. {}"
},
"modals": {
"_comment": "messages in modals",

View File

@ -323,13 +323,25 @@
"confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:"
},
"recovery_key": {
"key_connection_error": "Не удалось соединиться с сервером",
"key_synchronizing": "Синхронизация...",
"key_main_header": "Ключ восстановления",
"key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.",
"key_amount_toggle": "Ограничить использования",
"key_amount_field_title": "Макс. кол-во использований",
"key_duedate_toggle": "Ограничить срок использования",
"key_duedate_field_title": "Дата окончания срока",
"key_receive_button": "Получить ключ"
"key_receive_button": "Получить ключ",
"key_valid": "Ваш ключ действителен",
"key_invalid": "Ваш ключ больше не действителен",
"key_valid_until": "Действителен до {}",
"key_valid_for": "Можно использовать ещё {} раз",
"key_creation_date": "Создан {}",
"key_replace_button": "Сгенерировать новый ключ",
"key_receiving_description": "Запишите этот ключ в безопасном месте. Он предоставляет полный доступ к вашему серверу:",
"key_receiving_info": "Этот ключ больше не будет показан, но вы сможете заменить его новым.",
"key_receiving_done": "Готово!",
"generation_error": "Не удалось сгенерировать ключ. {}"
},
"modals": {
"_comment": "messages in modals",

View File

@ -109,6 +109,9 @@ class BNames {
/// A boolean field of [serverInstallationBox] box.
static String isServerResetedSecondTime = 'isServerResetedSecondTime';
/// A boolean field of [serverInstallationBox] box.
static String isRecoveringServer = 'isRecoveringServer';
/// Deprecated users box as it is unencrypted
static String usersDeprecated = 'users';

View File

@ -664,7 +664,8 @@ class ServerApi extends ApiMap {
var client = await getClient();
var data = {};
if (expiration != null) {
data['expiration'] = expiration.toIso8601String();
data['expiration'] = '${expiration.toIso8601String()}Z';
print(data['expiration']);
}
if (uses != null) {
data['uses'] = uses;

View File

@ -14,7 +14,16 @@ class RecoveryDeviceFormCubit extends FormCubit {
@override
FutureOr<void> onSubmit() async {
installationCubit.tryToRecover(tokenField.state.value, recoveryMethod);
late final String token;
// Trim spaces and make lowercase
if (recoveryMethod == ServerRecoveryMethods.recoveryKey ||
recoveryMethod == ServerRecoveryMethods.newDeviceKey) {
token = tokenField.state.value.trim().toLowerCase();
} else {
token = tokenField.state.value.trim();
}
installationCubit.tryToRecover(token, recoveryMethod);
}
final ServerInstallationCubit installationCubit;

View File

@ -287,6 +287,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
await repository.getRecoveryCapabilities(serverDomain);
await repository.saveDomain(serverDomain);
await repository.saveIsRecoveringServer(true);
emit(ServerInstallationRecovery(
serverDomain: serverDomain,
@ -458,6 +459,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
await repository.saveIsServerResetedFirstTime(true);
await repository.saveIsServerResetedSecondTime(true);
await repository.saveHasFinalChecked(true);
await repository.saveIsRecoveringServer(false);
final mainUser = await repository.getMainUser();
final updatedState = (state as ServerInstallationRecovery).copyWith(
backblazeCredential: backblazeCredential,

View File

@ -62,7 +62,8 @@ class ServerInstallationRepository {
);
}
if (serverDomain != null && serverDomain.provider == DnsProvider.unknown) {
if (box.get(BNames.isRecoveringServer, defaultValue: false) &&
serverDomain != null) {
return ServerInstallationRecovery(
hetznerKey: hetznerToken,
cloudFlareKey: cloudflareToken,
@ -601,6 +602,10 @@ class ServerInstallationRepository {
await box.put(BNames.rootUser, rootUser);
}
Future<void> saveIsRecoveringServer(bool value) async {
await box.put(BNames.isRecoveringServer, value);
}
Future<void> saveHasFinalChecked(bool value) async {
await box.put(BNames.hasFinalChecked, value);
}

View File

@ -16,12 +16,12 @@ class FilledButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ButtonStyle _enabledStyle = ElevatedButton.styleFrom(
final ButtonStyle enabledStyle = ElevatedButton.styleFrom(
onPrimary: Theme.of(context).colorScheme.onPrimary,
primary: Theme.of(context).colorScheme.primary,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0));
final ButtonStyle _disabledStyle = ElevatedButton.styleFrom(
final ButtonStyle disabledStyle = ElevatedButton.styleFrom(
onPrimary: Theme.of(context).colorScheme.onSurface.withAlpha(30),
primary: Theme.of(context).colorScheme.onSurface.withAlpha(98),
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0));
@ -33,7 +33,7 @@ class FilledButton extends StatelessWidget {
),
child: ElevatedButton(
onPressed: onPressed,
style: disabled ? _disabledStyle : _enabledStyle,
style: disabled ? disabledStyle : enabledStyle,
child: child ?? Text(title ?? ''),
),
);

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
class BrandCards {
static Widget big({required Widget child}) => _BrandCard(
@ -23,7 +22,9 @@ class BrandCards {
static Widget outlined({required Widget child}) => _OutlinedCard(
child: child,
);
static Widget filled({required Widget child}) => _FilledCard(
static Widget filled({required Widget child, bool tertiary = false}) =>
_FilledCard(
tertiary: tertiary,
child: child,
);
}
@ -80,12 +81,11 @@ class _OutlinedCard extends StatelessWidget {
}
class _FilledCard extends StatelessWidget {
const _FilledCard({
Key? key,
required this.child,
}) : super(key: key);
const _FilledCard({Key? key, required this.child, required this.tertiary})
: super(key: key);
final Widget child;
final bool tertiary;
@override
Widget build(BuildContext context) {
return Card(
@ -94,7 +94,9 @@ class _FilledCard extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(12)),
),
clipBehavior: Clip.antiAlias,
color: Theme.of(context).colorScheme.surfaceVariant,
color: tertiary
? Theme.of(context).colorScheme.tertiaryContainer
: Theme.of(context).colorScheme.surfaceVariant,
child: child,
);
}

View File

@ -1,13 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
@ -26,6 +24,9 @@ class MorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
@ -39,52 +40,53 @@ class MorePage extends StatelessWidget {
padding: paddingH15V0,
child: Column(
children: [
const BrandDivider(),
_NavItem(
title: 'more.configuration_wizard'.tr(),
iconData: BrandIcons.triangle,
goTo: const InitializingPage(),
),
_NavItem(
if (!isReady)
_MoreMenuItem(
title: 'more.configuration_wizard'.tr(),
iconData: Icons.change_history_outlined,
goTo: const InitializingPage(),
subtitle: 'not_ready_card.in_menu'.tr(),
accent: true,
),
if (isReady)
_MoreMenuItem(
title: 'more.create_ssh_key'.tr(),
iconData: Ionicons.key_outline,
goTo: SshKeysPage(
user: context.read<UsersCubit>().state.rootUser,
)),
if (isReady)
_MoreMenuItem(
iconData: Icons.password_outlined,
goTo: const RecoveryKey(),
title: 'recovery_key.key_main_header'.tr(),
),
_MoreMenuItem(
title: 'more.settings.title'.tr(),
iconData: BrandIcons.settings,
iconData: Icons.settings_outlined,
goTo: const AppSettingsPage(),
),
_NavItem(
_MoreMenuItem(
title: 'more.about_project'.tr(),
iconData: BrandIcons.engineer,
goTo: const AboutPage(),
),
_NavItem(
_MoreMenuItem(
title: 'more.about_app'.tr(),
iconData: BrandIcons.fire,
goTo: const InfoPage(),
),
_NavItem(
title: 'more.onboarding'.tr(),
iconData: BrandIcons.start,
goTo: const OnboardingPage(nextPage: RootPage()),
),
_NavItem(
if (!isReady)
_MoreMenuItem(
title: 'more.onboarding'.tr(),
iconData: BrandIcons.start,
goTo: const OnboardingPage(nextPage: RootPage()),
),
_MoreMenuItem(
title: 'more.console'.tr(),
iconData: BrandIcons.terminal,
goTo: const Console(),
),
_NavItem(
isEnabled: context.read<ServerInstallationCubit>().state
is ServerInstallationFinished,
title: 'more.create_ssh_key'.tr(),
iconData: Ionicons.key_outline,
goTo: SshKeysPage(
user: context.read<UsersCubit>().state.rootUser,
)),
_NavItem(
isEnabled: context.read<ServerInstallationCubit>().state
is ServerInstallationFinished,
iconData: Icons.password_outlined,
goTo: const RecoveryKey(),
title: 'recovery_key.key_main_header'.tr(),
)
],
),
)
@ -94,77 +96,53 @@ class MorePage extends StatelessWidget {
}
}
class _NavItem extends StatelessWidget {
const _NavItem({
Key? key,
this.isEnabled = true,
required this.iconData,
required this.goTo,
required this.title,
}) : super(key: key);
final IconData iconData;
final Widget goTo;
final String title;
final bool isEnabled;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: isEnabled
? () => Navigator.of(context).push(materialRoute(goTo))
: null,
child: _MoreMenuItem(
iconData: iconData,
title: title,
isActive: isEnabled,
),
);
}
}
class _MoreMenuItem extends StatelessWidget {
const _MoreMenuItem({
Key? key,
required this.iconData,
required this.title,
required this.isActive,
this.subtitle,
this.goTo,
this.accent = false,
}) : super(key: key);
final IconData iconData;
final String title;
final bool isActive;
final Widget? goTo;
final String? subtitle;
final bool accent;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color: BrandColors.dividerColor,
),
final color = accent
? Theme.of(context).colorScheme.onTertiaryContainer
: Theme.of(context).colorScheme.onSurface;
return BrandCards.filled(
tertiary: accent,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onTap: goTo != null
? () => Navigator.of(context).push(materialRoute(goTo!))
: null,
leading: Icon(
iconData,
size: 24,
color: color,
),
),
child: Row(
children: [
BrandText.body1(
title,
style: TextStyle(
color: isActive ? null : Colors.grey,
),
),
const Spacer(),
SizedBox(
width: 56,
child: Icon(
iconData,
size: 20,
color: isActive ? null : Colors.grey,
),
),
],
title: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: color,
),
),
subtitle: subtitle != null
? Text(
subtitle!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: color,
),
)
: null,
),
);
}

View File

@ -2,6 +2,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
@ -88,7 +89,7 @@ class _RecoveryKeyContentState extends State<RecoveryKeyContent> {
if (keyStatus.exists && !_isConfigurationVisible)
RecoveryKeyInformation(state: keyStatus),
if (_isConfigurationVisible || !keyStatus.exists)
RecoveryKeyConfiguration(),
const RecoveryKeyConfiguration(),
const SizedBox(height: 16),
if (!_isConfigurationVisible && keyStatus.isValid)
BrandButton.text(
@ -161,39 +162,42 @@ class RecoveryKeyInformation extends StatelessWidget {
@override
Widget build(BuildContext context) {
const padding = EdgeInsets.symmetric(vertical: 8.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.expiresAt != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_valid_until'.tr(
args: [state.expiresAt!.toIso8601String()],
const padding = EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0);
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.expiresAt != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_valid_until'.tr(
args: [DateFormat.yMMMMd().format(state.expiresAt!)],
),
),
),
),
if (state.usesLeft != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_valid_for'.tr(
args: [state.usesLeft!.toString()],
if (state.usesLeft != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_valid_for'.tr(
args: [state.usesLeft!.toString()],
),
),
),
),
if (state.generatedAt != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_creation_date'.tr(
args: [state.generatedAt!.toIso8601String()],
if (state.generatedAt != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_creation_date'.tr(
args: [DateFormat.yMMMMd().format(state.generatedAt!)],
),
textAlign: TextAlign.start,
),
textAlign: TextAlign.start,
),
),
],
],
),
);
}
}
@ -218,6 +222,38 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
DateTime _selectedDate = DateTime.now();
bool _isDateSelected = false;
bool _isLoading = false;
Future<void> _generateRecoveryToken() async {
setState(() {
_isLoading = true;
});
try {
final token = await context.read<RecoveryKeyCubit>().generateRecoveryKey(
numberOfUses:
_isAmountToggled ? int.tryParse(_amountController.text) : null,
expirationDate: _isExpirationToggled ? _selectedDate : null,
);
if (!mounted) return;
setState(() {
_isLoading = false;
});
Navigator.of(context).push(
materialRoute(
RecoveryKeyReceiving(recoveryKey: token), // TO DO
),
);
} on GenerationError catch (e) {
setState(() {
_isLoading = false;
});
getIt<NavigationService>().showSnackBar(
'recovery_key.generation_error'.tr(args: [e.message]),
);
return;
}
}
void _updateErrorStatuses() {
final amount = _amountController.text;
final expiration = _expirationController.text;
@ -241,8 +277,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
} else if (expiration.isEmpty) {
_isExpirationError = true;
} else {
_isExpirationError =
_selectedDate == null || _selectedDate.isBefore(DateTime.now());
_isExpirationError = _selectedDate.isBefore(DateTime.now());
}
});
@ -266,10 +301,10 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
value: _isAmountToggled,
title: Text('recovery_key.key_amount_toggle'.tr()),
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (bool toogled) {
onChanged: (bool toggled) {
setState(
() {
_isAmountToggled = toogled;
_isAmountToggled = toggled;
},
);
_updateErrorStatuses();
@ -287,7 +322,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
enabled: _isAmountToggled,
controller: _amountController,
decoration: InputDecoration(
border: OutlineInputBorder(),
border: const OutlineInputBorder(),
errorText: _isAmountError ? ' ' : null,
labelText: 'recovery_key.key_amount_field_title'.tr()),
keyboardType: TextInputType.number,
@ -304,10 +339,10 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
value: _isExpirationToggled,
title: Text('recovery_key.key_duedate_toggle'.tr()),
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (bool toogled) {
onChanged: (bool toggled) {
setState(
() {
_isExpirationToggled = toogled;
_isExpirationToggled = toggled;
},
);
_updateErrorStatuses();
@ -329,7 +364,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
},
readOnly: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
border: const OutlineInputBorder(),
errorText: _isExpirationError ? ' ' : null,
labelText: 'recovery_key.key_duedate_field_title'.tr()),
keyboardType: TextInputType.number,
@ -344,15 +379,9 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
const SizedBox(height: 16),
FilledButton(
title: 'recovery_key.key_receive_button'.tr(),
disabled: _isAmountError || _isExpirationError,
disabled: _isAmountError || _isExpirationError || _isLoading,
onPressed: !_isAmountError && !_isExpirationError
? () {
Navigator.of(context).push(
materialRoute(
const RecoveryKeyReceiving(recoveryKey: ''), // TO DO
),
);
}
? _generateRecoveryToken
: null,
),
],

View File

@ -15,14 +15,31 @@ class RecoveryKeyReceiving extends StatelessWidget {
Widget build(BuildContext context) {
return BrandHeroScreen(
heroTitle: 'recovery_key.key_main_header'.tr(),
heroSubtitle: 'recovering.method_select_description'.tr(),
heroSubtitle: 'recovery_key.key_receiving_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
Text(recoveryKey, style: Theme.of(context).textTheme.bodyLarge),
const Divider(),
const SizedBox(height: 16),
const Icon(Icons.info_outlined, size: 14),
Text('recovery_key.key_receiving_info'.tr()),
Text(
recoveryKey,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontFamily: 'RobotoMono',
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outlined, size: 24),
const SizedBox(height: 16),
Text('recovery_key.key_receiving_info'.tr()),
],
),
const SizedBox(height: 16),
FilledButton(
title: 'recovery_key.key_receiving_done'.tr(),

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:selfprivacy/logic/api_maps/server.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart';
import 'package:selfprivacy/ui/pages/more/more.dart';
import 'package:selfprivacy/ui/pages/providers/providers.dart';
@ -48,10 +47,11 @@ class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
super.dispose();
}
var selfprivacyServer = ServerApi();
@override
Widget build(BuildContext context) {
var isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
return SafeArea(
child: Provider<ChangeTab>(
create: (_) => ChangeTab(tabController.animateTo),
@ -68,36 +68,23 @@ class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
bottomNavigationBar: BrandTabBar(
controller: tabController,
),
floatingActionButton: SizedBox(
height: 104 + 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
ScaleTransition(
scale: _animation,
child: FloatingActionButton.small(
heroTag: 'new_user_fab',
onPressed: () {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: NewUser());
},
);
},
child: const Icon(Icons.person_add_outlined),
floatingActionButton: isReady
? SizedBox(
height: 104 + 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
ScaleTransition(
scale: _animation,
child: const AddUserFab(),
),
const SizedBox(height: 16),
const BrandFab(),
],
),
),
const SizedBox(height: 16),
const BrandFab(),
],
),
),
)
: null,
),
),
);

View File

@ -29,21 +29,17 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
children: [
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().keyId,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'KeyID',
labelText: 'KeyID',
),
),
const SizedBox(height: 16),
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().applicationKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Master Application Key',
labelText: 'Master Application Key',
),
),
const SizedBox(height: 16),

View File

@ -31,11 +31,9 @@ class RecoveryConfirmCloudflare extends StatelessWidget {
children: [
CubitFormTextField(
formFieldCubit: context.read<CloudFlareFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'initializing.5'.tr(),
labelText: 'initializing.5'.tr(),
),
),
const SizedBox(height: 16),

View File

@ -143,7 +143,7 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
),
),
leading: Icon(
Icons.dns,
Icons.dns_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
subtitle: Column(
@ -199,10 +199,11 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.warning_amber_outlined),
const SizedBox(height: 8),
const SizedBox(height: 16),
Text(
'recovering.modal_confirmation_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
],
),
@ -212,7 +213,9 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
children: <Widget>[
Text('recovering.modal_confirmation_description'.tr(),
style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 12),
const Divider(),
const SizedBox(height: 12),
Text(
server.name,
style: Theme.of(context).textTheme.titleMedium,
@ -275,19 +278,20 @@ class IsValidStringDisplay extends StatelessWidget {
? Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface)
: Icon(Icons.close, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 8),
isValid
? Text(
textIfValid,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
)
: Text(
textIfInvalid,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.error,
),
)
Expanded(
child: isValid
? Text(
textIfValid,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
)
: Text(
textIfInvalid,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.error,
),
)),
],
);
}

View File

@ -0,0 +1,25 @@
part of 'users.dart';
class AddUserFab extends StatelessWidget {
const AddUserFab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton.small(
heroTag: 'new_user_fab',
onPressed: () {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: const NewUser());
},
);
},
child: const Icon(Icons.person_add_outlined),
);
}
}

View File

@ -1,6 +1,8 @@
part of 'users.dart';
class NewUser extends StatelessWidget {
const NewUser({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var config = context.watch<ServerInstallationCubit>().state;

View File

@ -30,6 +30,7 @@ part 'empty.dart';
part 'new_user.dart';
part 'user.dart';
part 'user_details.dart';
part 'add_user_fab.dart';
class UsersPage extends StatelessWidget {
const UsersPage({Key? key}) : super(key: key);