diff --git a/assets/translations/en.json b/assets/translations/en.json index 73234e0c..e75e564c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 7b060a49..042998f4 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -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", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 28748a98..e3db5dc7 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -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'; diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index b58aff55..35c3c753 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -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; diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart index ddc35426..98c08f5c 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -14,7 +14,16 @@ class RecoveryDeviceFormCubit extends FormCubit { @override FutureOr 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; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 44e06862..20a89a52 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -287,6 +287,7 @@ class ServerInstallationCubit extends Cubit { await repository.getRecoveryCapabilities(serverDomain); await repository.saveDomain(serverDomain); + await repository.saveIsRecoveringServer(true); emit(ServerInstallationRecovery( serverDomain: serverDomain, @@ -458,6 +459,7 @@ class ServerInstallationCubit extends Cubit { 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, diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index a31ef764..f009f6ed 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -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 saveIsRecoveringServer(bool value) async { + await box.put(BNames.isRecoveringServer, value); + } + Future saveHasFinalChecked(bool value) async { await box.put(BNames.hasFinalChecked, value); } diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart index 7bf1b1dd..cc6aeb26 100644 --- a/lib/ui/components/brand_button/filled_button.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -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 ?? ''), ), ); diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 660054a8..7f19e47d 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -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, ); } diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 49b5e840..9c32128a 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -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().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().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().state - is ServerInstallationFinished, - title: 'more.create_ssh_key'.tr(), - iconData: Ionicons.key_outline, - goTo: SshKeysPage( - user: context.read().state.rootUser, - )), - _NavItem( - isEnabled: context.read().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, ), ); } diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 67280596..81a50067 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -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 { 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 { DateTime _selectedDate = DateTime.now(); bool _isDateSelected = false; + bool _isLoading = false; + + Future _generateRecoveryToken() async { + setState(() { + _isLoading = true; + }); + try { + final token = await context.read().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().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 { } 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 { 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 { 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 { 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 { }, 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 { 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, ), ], diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index edd3f89e..41f65a50 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -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(), diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index b14ecd52..9e0fcf67 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -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 with TickerProviderStateMixin { super.dispose(); } - var selfprivacyServer = ServerApi(); - @override Widget build(BuildContext context) { + var isReady = context.watch().state + is ServerInstallationFinished; + return SafeArea( child: Provider( create: (_) => ChangeTab(tabController.animateTo), @@ -68,36 +68,23 @@ class _RootPageState extends State 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( - 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, ), ), ); diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 646984a3..2a9fd8a9 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -29,21 +29,17 @@ class RecoveryConfirmBackblaze extends StatelessWidget { children: [ CubitFormTextField( formFieldCubit: context.read().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().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), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index 1006e390..28f1a8fc 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -31,11 +31,9 @@ class RecoveryConfirmCloudflare extends StatelessWidget { children: [ CubitFormTextField( formFieldCubit: context.read().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), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index e1b5403a..8242e521 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -143,7 +143,7 @@ class _RecoveryConfirmServerState extends State { ), ), leading: Icon( - Icons.dns, + Icons.dns_outlined, color: Theme.of(context).colorScheme.onSurface, ), subtitle: Column( @@ -199,10 +199,11 @@ class _RecoveryConfirmServerState extends State { 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 { children: [ 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, + ), + )), ], ); } diff --git a/lib/ui/pages/users/add_user_fab.dart b/lib/ui/pages/users/add_user_fab.dart new file mode 100644 index 00000000..c527a60b --- /dev/null +++ b/lib/ui/pages/users/add_user_fab.dart @@ -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( + 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), + ); + } +} diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 77a38918..4dd434b5 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -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().state; diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 9276b759..52bb430d 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -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);