feat: Include volume cost to overall monthly cost per server

pull/270/head
NaiJi ✨ 2023-08-06 20:28:02 -03:00
parent 11e745f822
commit 4f8f87f8a8
12 changed files with 306 additions and 204 deletions

View File

@ -341,6 +341,7 @@
"choose_server_type_ram": "{} GB of RAM", "choose_server_type_ram": "{} GB of RAM",
"choose_server_type_storage": "{} GB of system storage", "choose_server_type_storage": "{} GB of system storage",
"choose_server_type_payment_per_month": "{} per month", "choose_server_type_payment_per_month": "{} per month",
"choose_server_type_per_month_description": "{} for server and {} for storage",
"no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.", "no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.",
"dns_provider_bad_key_error": "API key is invalid", "dns_provider_bad_key_error": "API key is invalid",
"backblaze_bad_key_error": "Backblaze storage information is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid",
@ -542,4 +543,4 @@
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses" "cubit_statuses": "Cubit loading statuses"
} }
} }

View File

@ -336,6 +336,7 @@
"choose_server_type_ram": "{} GB у RAM", "choose_server_type_ram": "{} GB у RAM",
"choose_server_type_storage": "{} GB системного хранилища", "choose_server_type_storage": "{} GB системного хранилища",
"choose_server_type_payment_per_month": "{} в месяц", "choose_server_type_payment_per_month": "{} в месяц",
"choose_server_type_per_month_description": "{} за сервер и {} за хранилище",
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...", "no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
"dns_provider_bad_key_error": "API ключ неверен", "dns_provider_bad_key_error": "API ключ неверен",
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
@ -538,4 +539,4 @@
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.", "ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
"ignore_tls": "Не проверять сертификаты TLS" "ignore_tls": "Не проверять сертификаты TLS"
} }
} }

View File

@ -321,7 +321,7 @@ class DigitalOceanApi extends RestApiMap {
); );
} }
Future<GenericResult<DigitalOceanVolume?>> createVolume() async { Future<GenericResult<DigitalOceanVolume?>> createVolume(final int gb) async {
DigitalOceanVolume? volume; DigitalOceanVolume? volume;
Response? createVolumeResponse; Response? createVolumeResponse;
final Dio client = await getClient(); final Dio client = await getClient();
@ -331,7 +331,7 @@ class DigitalOceanApi extends RestApiMap {
createVolumeResponse = await client.post( createVolumeResponse = await client.post(
'/volumes', '/volumes',
data: { data: {
'size_gigabytes': 10, 'size_gigabytes': gb,
'name': 'volume${StringGenerators.storageName()}', 'name': 'volume${StringGenerators.storageName()}',
'labels': {'labelkey': 'value'}, 'labels': {'labelkey': 'value'},
'region': region, 'region': region,

View File

@ -382,7 +382,7 @@ class HetznerApi extends RestApiMap {
); );
} }
Future<GenericResult<HetznerVolume?>> createVolume() async { Future<GenericResult<HetznerVolume?>> createVolume(final int gb) async {
Response? createVolumeResponse; Response? createVolumeResponse;
HetznerVolume? volume; HetznerVolume? volume;
final Dio client = await getClient(); final Dio client = await getClient();
@ -390,7 +390,7 @@ class HetznerApi extends RestApiMap {
createVolumeResponse = await client.post( createVolumeResponse = await client.post(
'/volumes', '/volumes',
data: { data: {
'size': 10, 'size': gb,
'name': StringGenerators.storageName(), 'name': StringGenerators.storageName(),
'labels': {'labelkey': 'value'}, 'labels': {'labelkey': 'value'},
'location': region, 'location': region,

View File

@ -113,9 +113,11 @@ class ApiProviderVolumeCubit
return true; return true;
} }
Future<void> createVolume() async { Future<void> createVolume(final DiskSize size) async {
final ServerVolume? volume = final ServerVolume? volume = (await ProvidersController
(await ProvidersController.currentServerProvider!.createVolume()).data; .currentServerProvider!
.createVolume(size.gibibyte.toInt()))
.data;
final diskVolume = DiskVolume(providerVolume: volume); final diskVolume = DiskVolume(providerVolume: volume);
await attachVolume(diskVolume); await attachVolume(diskVolume);

View File

@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
@ -32,6 +33,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
Timer? timer; Timer? timer;
final DiskSize initialStorage = DiskSize.fromGibibyte(10);
Future<void> load() async { Future<void> load() async {
final ServerInstallationState state = await repository.load(); final ServerInstallationState state = await repository.load();
@ -257,6 +260,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
serverTypeId: state.serverTypeIdentificator!, serverTypeId: state.serverTypeIdentificator!,
errorCallback: clearAppConfig, errorCallback: clearAppConfig,
successCallback: onCreateServerSuccess, successCallback: onCreateServerSuccess,
storageSize: initialStorage,
); );
final result = final result =

View File

@ -1,3 +1,4 @@
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/hive/user.dart';
@ -11,6 +12,7 @@ class LaunchInstallationData {
required this.serverTypeId, required this.serverTypeId,
required this.errorCallback, required this.errorCallback,
required this.successCallback, required this.successCallback,
required this.storageSize,
}); });
final User rootUser; final User rootUser;
@ -20,4 +22,5 @@ class LaunchInstallationData {
final String serverTypeId; final String serverTypeId;
final Function() errorCallback; final Function() errorCallback;
final Function(ServerHostingDetails details) successCallback; final Function(ServerHostingDetails details) successCallback;
final DiskSize storageSize;
} }

View File

@ -254,7 +254,9 @@ class DigitalOceanServerProvider extends ServerProvider {
try { try {
final int dropletId = serverResult.data!; final int dropletId = serverResult.data!;
final newVolume = (await createVolume()).data; final newVolume =
(await createVolume(installationData.storageSize.gibibyte.toInt()))
.data;
final bool attachedVolume = (await _adapter.api().attachVolume( final bool attachedVolume = (await _adapter.api().attachVolume(
newVolume!.name, newVolume!.name,
dropletId, dropletId,
@ -588,10 +590,10 @@ class DigitalOceanServerProvider extends ServerProvider {
} }
@override @override
Future<GenericResult<ServerVolume?>> createVolume() async { Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
ServerVolume? volume; ServerVolume? volume;
final result = await _adapter.api().createVolume(); final result = await _adapter.api().createVolume(gb);
if (!result.success || result.data == null) { if (!result.success || result.data == null) {
return GenericResult( return GenericResult(
@ -708,13 +710,37 @@ class DigitalOceanServerProvider extends ServerProvider {
message: result.message, message: result.message,
); );
} }
final resultVolumes = await _adapter.api().getVolumes();
if (resultVolumes.data.isEmpty || !resultVolumes.success) {
return GenericResult(
success: false,
data: metadata,
code: resultVolumes.code,
message: resultVolumes.message,
);
}
final resultPricePerGb = await getPricePerGb();
if (resultPricePerGb.data == null || !resultPricePerGb.success) {
return GenericResult(
success: false,
data: metadata,
code: resultPricePerGb.code,
message: resultPricePerGb.message,
);
}
final List servers = result.data; final List servers = result.data;
final List<DigitalOceanVolume> volumes = resultVolumes.data;
final Price pricePerGb = resultPricePerGb.data!;
try { try {
final droplet = servers.firstWhere( final droplet = servers.firstWhere(
(final server) => server['id'] == serverId, (final server) => server['id'] == serverId,
); );
final volume = volumes.firstWhere(
(final volume) => droplet['volume_ids'].contains(volume.id),
);
metadata = [ metadata = [
ServerMetadataEntity( ServerMetadataEntity(
type: MetadataType.id, type: MetadataType.id,
@ -739,7 +765,8 @@ class DigitalOceanServerProvider extends ServerProvider {
ServerMetadataEntity( ServerMetadataEntity(
type: MetadataType.cost, type: MetadataType.cost,
trId: 'server.monthly_cost', trId: 'server.monthly_cost',
value: '${droplet['size']['price_monthly']} ${currency.shortcode}', value:
'${droplet['size']['price_monthly']} + ${(volume.sizeGigabytes * pricePerGb.value).toStringAsFixed(2)} ${currency.shortcode}',
), ),
ServerMetadataEntity( ServerMetadataEntity(
type: MetadataType.location, type: MetadataType.location,

View File

@ -164,7 +164,9 @@ class HetznerServerProvider extends ServerProvider {
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation( Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData, final LaunchInstallationData installationData,
) async { ) async {
final volumeResult = await _adapter.api().createVolume(); final volumeResult = await _adapter.api().createVolume(
installationData.storageSize.gibibyte.toInt(),
);
if (!volumeResult.success || volumeResult.data == null) { if (!volumeResult.success || volumeResult.data == null) {
return GenericResult( return GenericResult(
@ -614,10 +616,10 @@ class HetznerServerProvider extends ServerProvider {
} }
@override @override
Future<GenericResult<ServerVolume?>> createVolume() async { Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
ServerVolume? volume; ServerVolume? volume;
final result = await _adapter.api().createVolume(); final result = await _adapter.api().createVolume(gb);
if (!result.success || result.data == null) { if (!result.success || result.data == null) {
return GenericResult( return GenericResult(
@ -702,22 +704,45 @@ class HetznerServerProvider extends ServerProvider {
final int serverId, final int serverId,
) async { ) async {
List<ServerMetadataEntity> metadata = []; List<ServerMetadataEntity> metadata = [];
final result = await _adapter.api().getServers(); final resultServers = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) { if (resultServers.data.isEmpty || !resultServers.success) {
return GenericResult( return GenericResult(
success: false, success: false,
data: metadata, data: metadata,
code: result.code, code: resultServers.code,
message: result.message, message: resultServers.message,
);
}
final resultVolumes = await _adapter.api().getVolumes();
if (resultVolumes.data.isEmpty || !resultVolumes.success) {
return GenericResult(
success: false,
data: metadata,
code: resultVolumes.code,
message: resultVolumes.message,
);
}
final resultPricePerGb = await getPricePerGb();
if (resultPricePerGb.data == null || !resultPricePerGb.success) {
return GenericResult(
success: false,
data: metadata,
code: resultPricePerGb.code,
message: resultPricePerGb.message,
); );
} }
final List<HetznerServerInfo> servers = result.data; final List<HetznerServerInfo> servers = resultServers.data;
final List<HetznerVolume> volumes = resultVolumes.data;
final Price pricePerGb = resultPricePerGb.data!;
try { try {
final HetznerServerInfo server = servers.firstWhere( final HetznerServerInfo server = servers.firstWhere(
(final server) => server.id == serverId, (final server) => server.id == serverId,
); );
final HetznerVolume volume = volumes
.firstWhere((final volume) => server.volumes.contains(volume.id));
metadata = [ metadata = [
ServerMetadataEntity( ServerMetadataEntity(
type: MetadataType.id, type: MetadataType.id,
@ -743,7 +768,7 @@ class HetznerServerProvider extends ServerProvider {
type: MetadataType.cost, type: MetadataType.cost,
trId: 'server.monthly_cost', trId: 'server.monthly_cost',
value: value:
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} ${currency.shortcode}', '${server.serverType.prices[1].monthly.toStringAsFixed(2)} + ${(volume.size * pricePerGb.value).toStringAsFixed(2)} ${currency.shortcode}',
), ),
ServerMetadataEntity( ServerMetadataEntity(
type: MetadataType.location, type: MetadataType.location,

View File

@ -101,7 +101,7 @@ abstract class ServerProvider {
/// Tries to create an empty unattached [ServerVolume]. /// Tries to create an empty unattached [ServerVolume].
/// ///
/// If success, returns this volume information. /// If success, returns this volume information.
Future<GenericResult<ServerVolume?>> createVolume(); Future<GenericResult<ServerVolume?>> createVolume(final int gb);
/// Tries to delete the requested accessible [ServerVolume]. /// Tries to delete the requested accessible [ServerVolume].
Future<GenericResult<void>> deleteVolume(final ServerVolume volume); Future<GenericResult<void>> deleteVolume(final ServerVolume volume);

View File

@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
@ -31,6 +32,7 @@ class InitializingPage extends StatelessWidget {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final cubit = context.watch<ServerInstallationCubit>(); final cubit = context.watch<ServerInstallationCubit>();
final volumeCubit = context.read<ApiProviderVolumeCubit>();
if (cubit.state is ServerInstallationRecovery) { if (cubit.state is ServerInstallationRecovery) {
return const RecoveryRouting(); return const RecoveryRouting();
@ -39,7 +41,7 @@ class InitializingPage extends StatelessWidget {
if (cubit.state is! ServerInstallationFinished) { if (cubit.state is! ServerInstallationFinished) {
actualInitializingPage = [ actualInitializingPage = [
() => _stepServerProviderToken(cubit), () => _stepServerProviderToken(cubit),
() => _stepServerType(cubit), () => _stepServerType(cubit, volumeCubit),
() => _stepDnsProviderToken(cubit), () => _stepDnsProviderToken(cubit),
() => _stepBackblaze(cubit), () => _stepBackblaze(cubit),
() => _stepDomain(cubit), () => _stepDomain(cubit),
@ -226,6 +228,7 @@ class InitializingPage extends StatelessWidget {
Widget _stepServerType( Widget _stepServerType(
final ServerInstallationCubit serverInstallationCubit, final ServerInstallationCubit serverInstallationCubit,
final ApiProviderVolumeCubit apiProviderVolumeCubit,
) => ) =>
BlocProvider( BlocProvider(
create: (final context) => create: (final context) =>
@ -233,6 +236,7 @@ class InitializingPage extends StatelessWidget {
child: Builder( child: Builder(
builder: (final context) => ServerTypePicker( builder: (final context) => ServerTypePicker(
serverInstallationCubit: serverInstallationCubit, serverInstallationCubit: serverInstallationCubit,
apiProviderVolumeCubit: apiProviderVolumeCubit,
), ),
), ),
); );

View File

@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/illustrations/stray_deer.dart'; import 'package:selfprivacy/illustrations/stray_deer.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
@ -12,10 +14,12 @@ import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
class ServerTypePicker extends StatefulWidget { class ServerTypePicker extends StatefulWidget {
const ServerTypePicker({ const ServerTypePicker({
required this.serverInstallationCubit, required this.serverInstallationCubit,
required this.apiProviderVolumeCubit,
super.key, super.key,
}); });
final ServerInstallationCubit serverInstallationCubit; final ServerInstallationCubit serverInstallationCubit;
final ApiProviderVolumeCubit apiProviderVolumeCubit;
@override @override
State<ServerTypePicker> createState() => _ServerTypePickerState(); State<ServerTypePicker> createState() => _ServerTypePickerState();
@ -43,6 +47,7 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
return SelectTypePage( return SelectTypePage(
location: serverProviderLocation!, location: serverProviderLocation!,
serverInstallationCubit: widget.serverInstallationCubit, serverInstallationCubit: widget.serverInstallationCubit,
apiProviderVolumeCubit: widget.apiProviderVolumeCubit,
backToLocationPickingCallback: () { backToLocationPickingCallback: () {
setServerProviderLocation(null); setServerProviderLocation(null);
}, },
@ -145,202 +150,232 @@ class SelectTypePage extends StatelessWidget {
required this.backToLocationPickingCallback, required this.backToLocationPickingCallback,
required this.location, required this.location,
required this.serverInstallationCubit, required this.serverInstallationCubit,
required this.apiProviderVolumeCubit,
super.key, super.key,
}); });
final ServerProviderLocation location; final ServerProviderLocation location;
final ServerInstallationCubit serverInstallationCubit; final ServerInstallationCubit serverInstallationCubit;
final ApiProviderVolumeCubit apiProviderVolumeCubit;
final Function backToLocationPickingCallback; final Function backToLocationPickingCallback;
@override @override
Widget build(final BuildContext context) => FutureBuilder( Widget build(final BuildContext context) {
future: serverInstallationCubit.fetchAvailableTypesByLocation(location), final Future<List<ServerType>> serverTypes =
builder: ( serverInstallationCubit.fetchAvailableTypesByLocation(location);
final BuildContext context, final Future<Price?> pricePerGb = apiProviderVolumeCubit.getPricePerGb();
final AsyncSnapshot<Object?> snapshot, return FutureBuilder(
) { future: Future.wait([
if (snapshot.hasData) { serverTypes,
if ((snapshot.data as List<ServerType>).isEmpty) { pricePerGb,
return Column( ]),
crossAxisAlignment: CrossAxisAlignment.start, builder: (
children: [ final BuildContext context,
Text( final AsyncSnapshot<List<dynamic>> snapshot,
'initializing.locations_not_found'.tr(), ) {
style: Theme.of(context).textTheme.headlineSmall, if (snapshot.hasData) {
), if ((snapshot.data![0] as List<ServerType>).isEmpty ||
const SizedBox(height: 16), (snapshot.data![1] as Price?) == null) {
Text( return Column(
'initializing.locations_not_found_text'.tr(), crossAxisAlignment: CrossAxisAlignment.start,
style: Theme.of(context).textTheme.bodyMedium, children: [
), Text(
LayoutBuilder( 'initializing.locations_not_found'.tr(),
builder: (final context, final constraints) => CustomPaint( style: Theme.of(context).textTheme.headlineSmall,
size: Size( ),
constraints.maxWidth, const SizedBox(height: 16),
(constraints.maxWidth * 1).toDouble(), Text(
), 'initializing.locations_not_found_text'.tr(),
painter: StrayDeerPainter( style: Theme.of(context).textTheme.bodyMedium,
colorScheme: Theme.of(context).colorScheme, ),
colorPalette: context LayoutBuilder(
.read<AppSettingsCubit>() builder: (final context, final constraints) => CustomPaint(
.state size: Size(
.corePaletteOrDefault, constraints.maxWidth,
), (constraints.maxWidth * 1).toDouble(),
),
painter: StrayDeerPainter(
colorScheme: Theme.of(context).colorScheme,
colorPalette: context
.read<AppSettingsCubit>()
.state
.corePaletteOrDefault,
), ),
), ),
const SizedBox(height: 16), ),
BrandButton.rised( const SizedBox(height: 16),
onPressed: () { BrandButton.rised(
backToLocationPickingCallback(); onPressed: () {
}, backToLocationPickingCallback();
text: 'initializing.back_to_locations'.tr(), },
), text: 'initializing.back_to_locations'.tr(),
], ),
); ],
} );
return ResponsiveLayoutWithInfobox( }
topChild: Column( return ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'initializing.choose_server_type'.tr(), Text(
style: Theme.of(context).textTheme.headlineSmall, 'initializing.choose_server_type'.tr(),
), style: Theme.of(context).textTheme.headlineSmall,
const SizedBox(height: 16), ),
Text( const SizedBox(height: 16),
'initializing.choose_server_type_text'.tr(), Text(
style: Theme.of(context).textTheme.bodyMedium, 'initializing.choose_server_type_text'.tr(),
), style: Theme.of(context).textTheme.bodyMedium,
], ),
), ],
primaryColumn: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, primaryColumn: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
...(snapshot.data! as List<ServerType>).map( children: [
(final type) => Column( ...(snapshot.data![0] as List<ServerType>).map(
children: [ (final type) => Column(
SizedBox( children: [
width: double.infinity, SizedBox(
child: InkWell( width: double.infinity,
onTap: () { child: InkWell(
serverInstallationCubit.setServerType(type); onTap: () {
}, serverInstallationCubit.setServerType(type);
child: Card( },
child: Padding( child: Card(
padding: const EdgeInsets.all(16.0), child: Padding(
child: Column( padding: const EdgeInsets.all(16.0),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
type.title, Text(
style: Theme.of(context) type.title,
.textTheme style:
.titleMedium, Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
Icon( Icon(
Icons.memory_outlined, Icons.memory_outlined,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface, .onSurface,
),
const SizedBox(width: 8),
Text(
'server.core_count'.plural(type.cores),
style: Theme.of(context)
.textTheme
.bodyMedium,
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.memory_outlined,
color: Theme.of(context)
.colorScheme
.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_ram'
.tr(args: [type.ram.toString()]),
style: Theme.of(context)
.textTheme
.bodyMedium,
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.sd_card_outlined,
color: Theme.of(context)
.colorScheme
.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_storage'
.tr(
args: [type.disk.gibibyte.toString()],
), ),
const SizedBox(width: 8), style: Theme.of(context)
Text( .textTheme
'server.core_count' .bodyMedium,
.plural(type.cores), ),
style: Theme.of(context) ],
.textTheme ),
.bodyMedium, const SizedBox(height: 8),
const Divider(height: 8),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.payments_outlined,
color: Theme.of(context)
.colorScheme
.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_payment_per_month'
.tr(
args: [
'${type.price.value + (serverInstallationCubit.initialStorage.gibibyte * (snapshot.data![1] as Price).value)} ${type.price.currency.shortcode}'
],
), ),
], style: Theme.of(context)
), .textTheme
const SizedBox(height: 8), .bodyLarge,
Row( ),
children: [ ],
Icon( ),
Icons.memory_outlined, Row(
color: Theme.of(context) children: [
.colorScheme Icon(
.onSurface, Icons.info_outline,
color: Theme.of(context)
.colorScheme
.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_per_month_description'
.tr(
args: [
type.price.value.toString(),
'${serverInstallationCubit.initialStorage.gibibyte * (snapshot.data![1] as Price).value}',
],
), ),
const SizedBox(width: 8), style: Theme.of(context)
Text( .textTheme
'initializing.choose_server_type_ram' .bodyLarge,
.tr(args: [type.ram.toString()]), ),
style: Theme.of(context) ],
.textTheme ),
.bodyMedium, ],
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.sd_card_outlined,
color: Theme.of(context)
.colorScheme
.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_storage'
.tr(
args: [
type.disk.gibibyte.toString()
],
),
style: Theme.of(context)
.textTheme
.bodyMedium,
),
],
),
const SizedBox(height: 8),
const Divider(height: 8),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.payments_outlined,
color: Theme.of(context)
.colorScheme
.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_payment_per_month'
.tr(
args: [
'${type.price.value.toString()} ${type.price.currency.shortcode}'
],
),
style: Theme.of(context)
.textTheme
.bodyLarge,
),
],
),
],
),
), ),
), ),
), ),
), ),
const SizedBox(height: 8), ),
], const SizedBox(height: 8),
), ],
), ),
], ),
), ],
secondaryColumn: ),
InfoBox(text: 'initializing.choose_server_type_notice'.tr()), secondaryColumn:
); InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
} else { );
return const Center(child: CircularProgressIndicator()); } else {
} return const Center(child: CircularProgressIndicator());
}, }
); },
);
}
} }