mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-17 22:29:15 +00:00
feat: Include volume cost to overall monthly cost per server
This commit is contained in:
parent
11e745f822
commit
4f8f87f8a8
|
@ -341,6 +341,7 @@
|
|||
"choose_server_type_ram": "{} GB of RAM",
|
||||
"choose_server_type_storage": "{} GB of system storage",
|
||||
"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.",
|
||||
"dns_provider_bad_key_error": "API key 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",
|
||||
"cubit_statuses": "Cubit loading statuses"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -336,6 +336,7 @@
|
|||
"choose_server_type_ram": "{} GB у RAM",
|
||||
"choose_server_type_storage": "{} GB системного хранилища",
|
||||
"choose_server_type_payment_per_month": "{} в месяц",
|
||||
"choose_server_type_per_month_description": "{} за сервер и {} за хранилище",
|
||||
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
|
||||
"dns_provider_bad_key_error": "API ключ неверен",
|
||||
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
|
||||
|
@ -538,4 +539,4 @@
|
|||
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
|
||||
"ignore_tls": "Не проверять сертификаты TLS"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -321,7 +321,7 @@ class DigitalOceanApi extends RestApiMap {
|
|||
);
|
||||
}
|
||||
|
||||
Future<GenericResult<DigitalOceanVolume?>> createVolume() async {
|
||||
Future<GenericResult<DigitalOceanVolume?>> createVolume(final int gb) async {
|
||||
DigitalOceanVolume? volume;
|
||||
Response? createVolumeResponse;
|
||||
final Dio client = await getClient();
|
||||
|
@ -331,7 +331,7 @@ class DigitalOceanApi extends RestApiMap {
|
|||
createVolumeResponse = await client.post(
|
||||
'/volumes',
|
||||
data: {
|
||||
'size_gigabytes': 10,
|
||||
'size_gigabytes': gb,
|
||||
'name': 'volume${StringGenerators.storageName()}',
|
||||
'labels': {'labelkey': 'value'},
|
||||
'region': region,
|
||||
|
|
|
@ -382,7 +382,7 @@ class HetznerApi extends RestApiMap {
|
|||
);
|
||||
}
|
||||
|
||||
Future<GenericResult<HetznerVolume?>> createVolume() async {
|
||||
Future<GenericResult<HetznerVolume?>> createVolume(final int gb) async {
|
||||
Response? createVolumeResponse;
|
||||
HetznerVolume? volume;
|
||||
final Dio client = await getClient();
|
||||
|
@ -390,7 +390,7 @@ class HetznerApi extends RestApiMap {
|
|||
createVolumeResponse = await client.post(
|
||||
'/volumes',
|
||||
data: {
|
||||
'size': 10,
|
||||
'size': gb,
|
||||
'name': StringGenerators.storageName(),
|
||||
'labels': {'labelkey': 'value'},
|
||||
'location': region,
|
||||
|
|
|
@ -113,9 +113,11 @@ class ApiProviderVolumeCubit
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<void> createVolume() async {
|
||||
final ServerVolume? volume =
|
||||
(await ProvidersController.currentServerProvider!.createVolume()).data;
|
||||
Future<void> createVolume(final DiskSize size) async {
|
||||
final ServerVolume? volume = (await ProvidersController
|
||||
.currentServerProvider!
|
||||
.createVolume(size.gibibyte.toInt()))
|
||||
.data;
|
||||
|
||||
final diskVolume = DiskVolume(providerVolume: volume);
|
||||
await attachVolume(diskVolume);
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:equatable/equatable.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/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/callback_dialogue_branching.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
|
@ -32,6 +33,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
|
||||
Timer? timer;
|
||||
|
||||
final DiskSize initialStorage = DiskSize.fromGibibyte(10);
|
||||
|
||||
Future<void> load() async {
|
||||
final ServerInstallationState state = await repository.load();
|
||||
|
||||
|
@ -257,6 +260,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
serverTypeId: state.serverTypeIdentificator!,
|
||||
errorCallback: clearAppConfig,
|
||||
successCallback: onCreateServerSuccess,
|
||||
storageSize: initialStorage,
|
||||
);
|
||||
|
||||
final result =
|
||||
|
|
|
@ -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_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
|
@ -11,6 +12,7 @@ class LaunchInstallationData {
|
|||
required this.serverTypeId,
|
||||
required this.errorCallback,
|
||||
required this.successCallback,
|
||||
required this.storageSize,
|
||||
});
|
||||
|
||||
final User rootUser;
|
||||
|
@ -20,4 +22,5 @@ class LaunchInstallationData {
|
|||
final String serverTypeId;
|
||||
final Function() errorCallback;
|
||||
final Function(ServerHostingDetails details) successCallback;
|
||||
final DiskSize storageSize;
|
||||
}
|
||||
|
|
|
@ -254,7 +254,9 @@ class DigitalOceanServerProvider extends ServerProvider {
|
|||
|
||||
try {
|
||||
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(
|
||||
newVolume!.name,
|
||||
dropletId,
|
||||
|
@ -588,10 +590,10 @@ class DigitalOceanServerProvider extends ServerProvider {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<GenericResult<ServerVolume?>> createVolume() async {
|
||||
Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
|
||||
ServerVolume? volume;
|
||||
|
||||
final result = await _adapter.api().createVolume();
|
||||
final result = await _adapter.api().createVolume(gb);
|
||||
|
||||
if (!result.success || result.data == null) {
|
||||
return GenericResult(
|
||||
|
@ -708,13 +710,37 @@ class DigitalOceanServerProvider extends ServerProvider {
|
|||
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<DigitalOceanVolume> volumes = resultVolumes.data;
|
||||
final Price pricePerGb = resultPricePerGb.data!;
|
||||
try {
|
||||
final droplet = servers.firstWhere(
|
||||
(final server) => server['id'] == serverId,
|
||||
);
|
||||
|
||||
final volume = volumes.firstWhere(
|
||||
(final volume) => droplet['volume_ids'].contains(volume.id),
|
||||
);
|
||||
|
||||
metadata = [
|
||||
ServerMetadataEntity(
|
||||
type: MetadataType.id,
|
||||
|
@ -739,7 +765,8 @@ class DigitalOceanServerProvider extends ServerProvider {
|
|||
ServerMetadataEntity(
|
||||
type: MetadataType.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(
|
||||
type: MetadataType.location,
|
||||
|
|
|
@ -164,7 +164,9 @@ class HetznerServerProvider extends ServerProvider {
|
|||
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
||||
final LaunchInstallationData installationData,
|
||||
) async {
|
||||
final volumeResult = await _adapter.api().createVolume();
|
||||
final volumeResult = await _adapter.api().createVolume(
|
||||
installationData.storageSize.gibibyte.toInt(),
|
||||
);
|
||||
|
||||
if (!volumeResult.success || volumeResult.data == null) {
|
||||
return GenericResult(
|
||||
|
@ -614,10 +616,10 @@ class HetznerServerProvider extends ServerProvider {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<GenericResult<ServerVolume?>> createVolume() async {
|
||||
Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
|
||||
ServerVolume? volume;
|
||||
|
||||
final result = await _adapter.api().createVolume();
|
||||
final result = await _adapter.api().createVolume(gb);
|
||||
|
||||
if (!result.success || result.data == null) {
|
||||
return GenericResult(
|
||||
|
@ -702,22 +704,45 @@ class HetznerServerProvider extends ServerProvider {
|
|||
final int serverId,
|
||||
) async {
|
||||
List<ServerMetadataEntity> metadata = [];
|
||||
final result = await _adapter.api().getServers();
|
||||
if (result.data.isEmpty || !result.success) {
|
||||
final resultServers = await _adapter.api().getServers();
|
||||
if (resultServers.data.isEmpty || !resultServers.success) {
|
||||
return GenericResult(
|
||||
success: false,
|
||||
data: metadata,
|
||||
code: result.code,
|
||||
message: result.message,
|
||||
code: resultServers.code,
|
||||
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 {
|
||||
final HetznerServerInfo server = servers.firstWhere(
|
||||
(final server) => server.id == serverId,
|
||||
);
|
||||
|
||||
final HetznerVolume volume = volumes
|
||||
.firstWhere((final volume) => server.volumes.contains(volume.id));
|
||||
|
||||
metadata = [
|
||||
ServerMetadataEntity(
|
||||
type: MetadataType.id,
|
||||
|
@ -743,7 +768,7 @@ class HetznerServerProvider extends ServerProvider {
|
|||
type: MetadataType.cost,
|
||||
trId: 'server.monthly_cost',
|
||||
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(
|
||||
type: MetadataType.location,
|
||||
|
|
|
@ -101,7 +101,7 @@ abstract class ServerProvider {
|
|||
/// Tries to create an empty unattached [ServerVolume].
|
||||
///
|
||||
/// If success, returns this volume information.
|
||||
Future<GenericResult<ServerVolume?>> createVolume();
|
||||
Future<GenericResult<ServerVolume?>> createVolume(final int gb);
|
||||
|
||||
/// Tries to delete the requested accessible [ServerVolume].
|
||||
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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/forms/factories/field_cubit_factory.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
|
||||
|
@ -31,6 +32,7 @@ class InitializingPage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final cubit = context.watch<ServerInstallationCubit>();
|
||||
final volumeCubit = context.read<ApiProviderVolumeCubit>();
|
||||
|
||||
if (cubit.state is ServerInstallationRecovery) {
|
||||
return const RecoveryRouting();
|
||||
|
@ -39,7 +41,7 @@ class InitializingPage extends StatelessWidget {
|
|||
if (cubit.state is! ServerInstallationFinished) {
|
||||
actualInitializingPage = [
|
||||
() => _stepServerProviderToken(cubit),
|
||||
() => _stepServerType(cubit),
|
||||
() => _stepServerType(cubit, volumeCubit),
|
||||
() => _stepDnsProviderToken(cubit),
|
||||
() => _stepBackblaze(cubit),
|
||||
() => _stepDomain(cubit),
|
||||
|
@ -226,6 +228,7 @@ class InitializingPage extends StatelessWidget {
|
|||
|
||||
Widget _stepServerType(
|
||||
final ServerInstallationCubit serverInstallationCubit,
|
||||
final ApiProviderVolumeCubit apiProviderVolumeCubit,
|
||||
) =>
|
||||
BlocProvider(
|
||||
create: (final context) =>
|
||||
|
@ -233,6 +236,7 @@ class InitializingPage extends StatelessWidget {
|
|||
child: Builder(
|
||||
builder: (final context) => ServerTypePicker(
|
||||
serverInstallationCubit: serverInstallationCubit,
|
||||
apiProviderVolumeCubit: apiProviderVolumeCubit,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'package:flutter/material.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_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_type.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 {
|
||||
const ServerTypePicker({
|
||||
required this.serverInstallationCubit,
|
||||
required this.apiProviderVolumeCubit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ServerInstallationCubit serverInstallationCubit;
|
||||
final ApiProviderVolumeCubit apiProviderVolumeCubit;
|
||||
|
||||
@override
|
||||
State<ServerTypePicker> createState() => _ServerTypePickerState();
|
||||
|
@ -43,6 +47,7 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
|
|||
return SelectTypePage(
|
||||
location: serverProviderLocation!,
|
||||
serverInstallationCubit: widget.serverInstallationCubit,
|
||||
apiProviderVolumeCubit: widget.apiProviderVolumeCubit,
|
||||
backToLocationPickingCallback: () {
|
||||
setServerProviderLocation(null);
|
||||
},
|
||||
|
@ -145,202 +150,232 @@ class SelectTypePage extends StatelessWidget {
|
|||
required this.backToLocationPickingCallback,
|
||||
required this.location,
|
||||
required this.serverInstallationCubit,
|
||||
required this.apiProviderVolumeCubit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ServerProviderLocation location;
|
||||
final ServerInstallationCubit serverInstallationCubit;
|
||||
final ApiProviderVolumeCubit apiProviderVolumeCubit;
|
||||
final Function backToLocationPickingCallback;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => FutureBuilder(
|
||||
future: serverInstallationCubit.fetchAvailableTypesByLocation(location),
|
||||
builder: (
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<Object?> snapshot,
|
||||
) {
|
||||
if (snapshot.hasData) {
|
||||
if ((snapshot.data as List<ServerType>).isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'initializing.locations_not_found'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'initializing.locations_not_found_text'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (final context, final constraints) => CustomPaint(
|
||||
size: Size(
|
||||
constraints.maxWidth,
|
||||
(constraints.maxWidth * 1).toDouble(),
|
||||
),
|
||||
painter: StrayDeerPainter(
|
||||
colorScheme: Theme.of(context).colorScheme,
|
||||
colorPalette: context
|
||||
.read<AppSettingsCubit>()
|
||||
.state
|
||||
.corePaletteOrDefault,
|
||||
),
|
||||
Widget build(final BuildContext context) {
|
||||
final Future<List<ServerType>> serverTypes =
|
||||
serverInstallationCubit.fetchAvailableTypesByLocation(location);
|
||||
final Future<Price?> pricePerGb = apiProviderVolumeCubit.getPricePerGb();
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
serverTypes,
|
||||
pricePerGb,
|
||||
]),
|
||||
builder: (
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<List<dynamic>> snapshot,
|
||||
) {
|
||||
if (snapshot.hasData) {
|
||||
if ((snapshot.data![0] as List<ServerType>).isEmpty ||
|
||||
(snapshot.data![1] as Price?) == null) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'initializing.locations_not_found'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'initializing.locations_not_found_text'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (final context, final constraints) => CustomPaint(
|
||||
size: Size(
|
||||
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(
|
||||
onPressed: () {
|
||||
backToLocationPickingCallback();
|
||||
},
|
||||
text: 'initializing.back_to_locations'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return ResponsiveLayoutWithInfobox(
|
||||
topChild: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'initializing.choose_server_type'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'initializing.choose_server_type_text'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
primaryColumn: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...(snapshot.data! as List<ServerType>).map(
|
||||
(final type) => Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
serverInstallationCubit.setServerType(type);
|
||||
},
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
type.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.memory_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BrandButton.rised(
|
||||
onPressed: () {
|
||||
backToLocationPickingCallback();
|
||||
},
|
||||
text: 'initializing.back_to_locations'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return ResponsiveLayoutWithInfobox(
|
||||
topChild: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'initializing.choose_server_type'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'initializing.choose_server_type_text'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
primaryColumn: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...(snapshot.data![0] as List<ServerType>).map(
|
||||
(final type) => Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
serverInstallationCubit.setServerType(type);
|
||||
},
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
type.title,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.memory_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.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),
|
||||
Text(
|
||||
'server.core_count'
|
||||
.plural(type.cores),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
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}'
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.memory_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
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),
|
||||
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()
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
secondaryColumn:
|
||||
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
secondaryColumn:
|
||||
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue