From 72760e7980142664b9bdde34eb95f44f7002d946 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sat, 15 Oct 2022 21:51:37 +0000 Subject: [PATCH] feat(initializing): Implement server type selection for initialization page --- assets/translations/en.json | 2 + assets/translations/ru.json | 2 + lib/config/hive_config.dart | 3 + .../server_installation_cubit.dart | 39 +++- .../server_installation_repository.dart | 6 + .../server_installation_state.dart | 34 +-- lib/logic/get_it/api_config.dart | 7 + lib/logic/models/disk_size.dart | 2 +- .../setup/initializing/initializing.dart | 12 +- .../initializing/server_type_picker.dart | 197 +++++++++++------- 10 files changed, 203 insertions(+), 101 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index bf8e4e2c..d2c75572 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -268,6 +268,8 @@ "place_where_data": "A place where your data and SelfPrivacy services will reside:", "how": "How to obtain API token", "provider_bad_key_error": "Provider API key is invalid", + "no_locations_found": "No available locations found. Make sure your account is accessible.", + "no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.", "cloudflare_bad_key_error": "Cloudflare API key is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid", "connect_cloudflare": "Connect CloudFlare", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 604edf35..98f3ff23 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -267,6 +267,8 @@ "place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:", "how": "Как получить API Token", "provider_bad_key_error": "API ключ провайдера неверен", + "no_locations_found": "Не найдено локаций. Убедитесь, что ваш аккаунт доступен.", + "no_server_types_found": "Не удалось получить список серверов. Убедитесь, что ваш аккаунт доступен и попытайтесь сменить локацию сервера.", "cloudflare_bad_key_error": "Cloudflare API ключ неверен", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", "connect_cloudflare": "Подключите CloudFlare", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 29ab0519..b8cc287c 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -90,6 +90,9 @@ class BNames { /// A String field of [serverInstallationBox] box. static String cloudFlareKey = 'cloudFlareKey'; + /// A String field of [serverTypeIdentifier] box. + static String serverTypeIdentifier = 'serverTypeIdentifier'; + /// A [User] field of [serverInstallationBox] box. static String rootUser = 'rootUser'; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 84410468..4f4da2e1 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -6,8 +6,6 @@ import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; @@ -82,7 +80,9 @@ class ServerInstallationCubit extends Cubit { repository.serverProviderApiFactory! .getServerProvider( settings: const ServerProviderApiSettings( - region: 'fra1', isWithToken: false), + region: 'fra1', + isWithToken: false, + ), ) .isApiTokenValid(providerToken); @@ -101,12 +101,25 @@ class ServerInstallationCubit extends Cubit { } return repository.serverProviderApiFactory! - .getServerProvider( - settings: const ServerProviderApiSettings(region: 'fra1'), - ) - .getAvailableLocations(); + .getServerProvider( + settings: const ServerProviderApiSettings(region: 'fra1'), + ) + .getAvailableLocations(); + } + + Future> fetchAvailableTypesByLocation( + final ServerProviderLocation location, + ) async { + if (repository.serverProviderApiFactory == null) { + return []; + } + + return repository.serverProviderApiFactory! + .getServerProvider( + settings: const ServerProviderApiSettings(region: 'fra1'), + ) + .getServerTypesByLocation(location: location); } - void setServerProviderKey(final String serverProviderKey) async { await repository.saveServerProviderKey(serverProviderKey); @@ -128,6 +141,16 @@ class ServerInstallationCubit extends Cubit { ); } + void setServerType(final String serverTypeId) async { + await repository.saveServerType(serverTypeId); + + emit( + (state as ServerInstallationNotFinished).copyWith( + serverTypeIdentificator: serverTypeId, + ), + ); + } + void setCloudflareKey(final String cloudFlareKey) async { if (state is ServerInstallationRecovery) { setAndValidateCloudflareToken(cloudFlareKey); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 4fe10b1f..404e60d1 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -50,6 +50,7 @@ class ServerInstallationRepository { Future load() async { final String? providerApiToken = getIt().serverProviderKey; final String? cloudflareToken = getIt().cloudFlareKey; + final String? serverTypeIdentificator = getIt().serverType; final ServerDomain? serverDomain = getIt().serverDomain; final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; @@ -73,6 +74,7 @@ class ServerInstallationRepository { if (box.get(BNames.hasFinalChecked, defaultValue: false)) { return ServerInstallationFinished( providerApiToken: providerApiToken!, + serverTypeIdentificator: serverTypeIdentificator!, cloudFlareKey: cloudflareToken!, serverDomain: serverDomain!, backblazeCredential: backblazeCredential!, @@ -697,6 +699,10 @@ class ServerInstallationRepository { await getIt().storeServerProviderKey(key); } + Future saveServerType(final String serverType) async { + await getIt().storeServerTypeIdentifier(serverType); + } + Future deleteServerProviderKey() async { await box.delete(BNames.hetznerKey); getIt().init(); diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 1a130baf..40a21d5d 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -3,7 +3,7 @@ part of '../server_installation/server_installation_cubit.dart'; abstract class ServerInstallationState extends Equatable { const ServerInstallationState({ required this.providerApiToken, - required this.serverType, + required this.serverTypeIdentificator, required this.cloudFlareKey, required this.backblazeCredential, required this.serverDomain, @@ -17,6 +17,7 @@ abstract class ServerInstallationState extends Equatable { @override List get props => [ providerApiToken, + serverTypeIdentificator, cloudFlareKey, backblazeCredential, serverDomain, @@ -28,17 +29,17 @@ abstract class ServerInstallationState extends Equatable { final String? providerApiToken; final String? cloudFlareKey; + final String? serverTypeIdentificator; final BackblazeCredential? backblazeCredential; final ServerDomain? serverDomain; final User? rootUser; final ServerHostingDetails? serverDetails; - final ServerType? serverType; final bool isServerStarted; final bool isServerResetedFirstTime; final bool isServerResetedSecondTime; bool get isServerProviderApiKeyFilled => providerApiToken != null; - bool get isServerTypeFilled => serverType != null; + bool get isServerTypeFilled => serverTypeIdentificator != null; bool get isDnsProviderFilled => cloudFlareKey != null; bool get isBackupsProviderFilled => backblazeCredential != null; bool get isDomainSelected => serverDomain != null; @@ -85,7 +86,7 @@ class TimerState extends ServerInstallationNotFinished { this.duration, }) : super( providerApiToken: dataState.providerApiToken, - serverType: dataState.serverType, + serverTypeIdentificator: dataState.serverTypeIdentificator, cloudFlareKey: dataState.cloudFlareKey, backblazeCredential: dataState.backblazeCredential, serverDomain: dataState.serverDomain, @@ -130,7 +131,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { required this.isLoading, required this.dnsMatches, super.providerApiToken, - super.serverType, + super.serverTypeIdentificator, super.cloudFlareKey, super.backblazeCredential, super.serverDomain, @@ -143,7 +144,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { @override List get props => [ providerApiToken, - serverType, + serverTypeIdentificator, cloudFlareKey, backblazeCredential, serverDomain, @@ -157,7 +158,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { ServerInstallationNotFinished copyWith({ final String? providerApiToken, - final ServerType? serverType, + final String? serverTypeIdentificator, final String? cloudFlareKey, final BackblazeCredential? backblazeCredential, final ServerDomain? serverDomain, @@ -171,7 +172,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { }) => ServerInstallationNotFinished( providerApiToken: providerApiToken ?? this.providerApiToken, - serverType: serverType ?? this.serverType, + serverTypeIdentificator: serverTypeIdentificator ?? this.serverTypeIdentificator, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, backblazeCredential: backblazeCredential ?? this.backblazeCredential, serverDomain: serverDomain ?? this.serverDomain, @@ -188,7 +189,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { ServerInstallationFinished finish() => ServerInstallationFinished( providerApiToken: providerApiToken!, - serverType: serverType!, + serverTypeIdentificator: serverTypeIdentificator!, cloudFlareKey: cloudFlareKey!, backblazeCredential: backblazeCredential!, serverDomain: serverDomain!, @@ -204,6 +205,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished { const ServerInstallationEmpty() : super( providerApiToken: null, + serverTypeIdentificator: null, cloudFlareKey: null, backblazeCredential: null, serverDomain: null, @@ -220,7 +222,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished { class ServerInstallationFinished extends ServerInstallationState { const ServerInstallationFinished({ required String super.providerApiToken, - required ServerType super.serverType, + required String super.serverTypeIdentificator, required String super.cloudFlareKey, required BackblazeCredential super.backblazeCredential, required ServerDomain super.serverDomain, @@ -234,7 +236,7 @@ class ServerInstallationFinished extends ServerInstallationState { @override List get props => [ providerApiToken, - serverType, + serverTypeIdentificator, cloudFlareKey, backblazeCredential, serverDomain, @@ -273,7 +275,7 @@ class ServerInstallationRecovery extends ServerInstallationState { required this.currentStep, required this.recoveryCapabilities, super.providerApiToken, - super.serverType, + super.serverTypeIdentificator, super.cloudFlareKey, super.backblazeCredential, super.serverDomain, @@ -290,7 +292,7 @@ class ServerInstallationRecovery extends ServerInstallationState { @override List get props => [ providerApiToken, - serverType, + serverTypeIdentificator, cloudFlareKey, backblazeCredential, serverDomain, @@ -303,7 +305,7 @@ class ServerInstallationRecovery extends ServerInstallationState { ServerInstallationRecovery copyWith({ final String? providerApiToken, - final ServerType? serverType, + final String? serverTypeIdentificator, final String? cloudFlareKey, final BackblazeCredential? backblazeCredential, final ServerDomain? serverDomain, @@ -314,7 +316,7 @@ class ServerInstallationRecovery extends ServerInstallationState { }) => ServerInstallationRecovery( providerApiToken: providerApiToken ?? this.providerApiToken, - serverType: serverType ?? this.serverType, + serverTypeIdentificator: serverTypeIdentificator ?? this.serverTypeIdentificator, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, backblazeCredential: backblazeCredential ?? this.backblazeCredential, serverDomain: serverDomain ?? this.serverDomain, @@ -326,7 +328,7 @@ class ServerInstallationRecovery extends ServerInstallationState { ServerInstallationFinished finish() => ServerInstallationFinished( providerApiToken: providerApiToken!, - serverType: serverType!, + serverTypeIdentificator: serverTypeIdentificator!, cloudFlareKey: cloudFlareKey!, backblazeCredential: backblazeCredential!, serverDomain: serverDomain!, diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 6a81141f..387d7e7f 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -10,6 +10,7 @@ class ApiConfigModel { ServerHostingDetails? get serverDetails => _serverDetails; String? get serverProviderKey => _serverProviderKey; + String? get serverType => _serverType; String? get cloudFlareKey => _cloudFlareKey; BackblazeCredential? get backblazeCredential => _backblazeCredential; ServerDomain? get serverDomain => _serverDomain; @@ -17,6 +18,7 @@ class ApiConfigModel { String? _serverProviderKey; String? _cloudFlareKey; + String? _serverType; ServerHostingDetails? _serverDetails; BackblazeCredential? _backblazeCredential; ServerDomain? _serverDomain; @@ -32,6 +34,11 @@ class ApiConfigModel { _cloudFlareKey = value; } + Future storeServerTypeIdentifier(final String typeIdentifier) async { + await _box.put(BNames.serverTypeIdentifier, typeIdentifier); + _serverType = typeIdentifier; + } + Future storeBackblazeCredential(final BackblazeCredential value) async { await _box.put(BNames.backblazeCredential, value); _backblazeCredential = value; diff --git a/lib/logic/models/disk_size.dart b/lib/logic/models/disk_size.dart index 44c6bc35..f7283689 100644 --- a/lib/logic/models/disk_size.dart +++ b/lib/logic/models/disk_size.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; class DiskSize { - const DiskSize({final this.byte = 0}); + const DiskSize({this.byte = 0}); DiskSize.fromKibibyte(final double kibibyte) : this(byte: (kibibyte * 1024).round()); diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index a211588b..029a30e4 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -140,15 +140,17 @@ class InitializingPage extends StatelessWidget { } } - Widget _stepServerProviderToken(final ServerInstallationCubit serverInstallationCubit) => + Widget _stepServerProviderToken( + final ServerInstallationCubit serverInstallationCubit) => ServerProviderPicker( serverInstallationCubit: serverInstallationCubit, ); - Widget _stepServerType(final ServerInstallationCubit serverInstallationCubit) => - ServerTypePicker( - serverInstallationCubit: serverInstallationCubit, - ); + Widget _stepServerType( + final ServerInstallationCubit serverInstallationCubit) => + ServerTypePicker( + serverInstallationCubit: serverInstallationCubit, + ); void _showModal(final BuildContext context, final Widget widget) { showModalBottomSheet( diff --git a/lib/ui/pages/setup/initializing/server_type_picker.dart b/lib/ui/pages/setup/initializing/server_type_picker.dart index 3dd7d3c9..2536aab9 100644 --- a/lib/ui/pages/setup/initializing/server_type_picker.dart +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -1,8 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; +import 'package:selfprivacy/logic/models/server_type.dart'; class ServerTypePicker extends StatefulWidget { const ServerTypePicker({ @@ -18,6 +19,7 @@ class ServerTypePicker extends StatefulWidget { class _ServerTypePickerState extends State { ServerProviderLocation? serverProviderLocation; + ServerType? serverType; void setServerProviderLocation(final ServerProviderLocation location) { setState(() { @@ -27,92 +29,145 @@ class _ServerTypePickerState extends State { @override Widget build(final BuildContext context) { - + if (serverProviderLocation == null) { + return SelectLocationPage( + serverInstallationCubit: widget.serverInstallationCubit, + callback: setServerProviderLocation, + ); + } + + return SelectTypePage( + location: serverProviderLocation!, + serverInstallationCubit: widget.serverInstallationCubit, + ); } } class SelectLocationPage extends StatelessWidget { const SelectLocationPage({ required this.serverInstallationCubit, - super.key, - }); - - final ServerInstallationCubit serverInstallationCubit; - - @override - Widget build(final BuildContext context) => FutureBuilder( - future: serverInstallationCubit.repository.serverProviderApiFactory, - builder: ( - final BuildContext context, - final AsyncSnapshot snapshot, - ) { - if (snapshot.hasData) { - return _KeyDisplay( - newDeviceKey: snapshot.data.toString(), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ), -} - -class ProviderSelectionPage extends StatelessWidget { - const ProviderSelectionPage({ required this.callback, super.key, }); final Function callback; + final ServerInstallationCubit serverInstallationCubit; @override - Widget build(final BuildContext context) => Column( - children: [ - Text( - 'initializing.select_provider'.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 10), - Text( - 'initializing.place_where_data'.tr(), - ), - const SizedBox(height: 10), - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 320, - ), - child: Row( + Widget build(final BuildContext context) => FutureBuilder( + future: serverInstallationCubit.fetchAvailableLocations(), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + if ((snapshot.data as List).isEmpty) { + return Text('initializing.no_locations_found'.tr()); + } + return ListView( + padding: paddingH15V0, children: [ - InkWell( - onTap: () { - context - .read() - .setServerProviderType(ServerProvider.hetzner); - callback(ServerProvider.hetzner); - }, - child: Image.asset( - 'assets/images/logos/hetzner.png', - width: 150, - ), - ), - const SizedBox( - width: 20, - ), - InkWell( - onTap: () { - context - .read() - .setServerProviderType(ServerProvider.digitalOcean); - callback(ServerProvider.digitalOcean); - }, - child: Image.asset( - 'assets/images/logos/digital_ocean.png', - width: 150, + ...(snapshot.data! as List).map( + (final location) => InkWell( + onTap: () { + callback(location); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (location.flag != null) Text(location.flag!), + const SizedBox(height: 8), + Text(location.title), + const SizedBox(height: 8), + if (location.description != null) + Text(location.description!), + ], + ), + ], + ), + ), + ), ), ), + const SizedBox(height: 24), ], - ), - ), - ], + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); +} + +class SelectTypePage extends StatelessWidget { + const SelectTypePage({ + required this.location, + required this.serverInstallationCubit, + super.key, + }); + + final ServerProviderLocation location; + final ServerInstallationCubit serverInstallationCubit; + + @override + Widget build(final BuildContext context) => FutureBuilder( + future: serverInstallationCubit.fetchAvailableTypesByLocation(location), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + if ((snapshot.data as List).isEmpty) { + return Text('initializing.no_server_types_found'.tr()); + } + return ListView( + padding: paddingH15V0, + children: [ + ...(snapshot.data! as List).map( + (final type) => InkWell( + onTap: () { + serverInstallationCubit.setServerType(type.identifier); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(type.title), + const SizedBox(height: 8), + Text('cores: $type.cores'), + const SizedBox(height: 8), + Text('ram: $type.ram'), + const SizedBox(height: 8), + Text('disk: $type.disk.gibibyte'), + const SizedBox(height: 8), + Text('price: $type.price.value $type.price.currency'), + ], + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 24), + ], + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, ); }