mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-08 00:51:20 +00:00
Merge pull request 'feat: Include volume and ipv4 costs to overall monthly cost per server' (#270) from price-calculation into master
Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/pulls/270 Reviewed-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
commit
c5671cc767
|
@ -124,6 +124,7 @@
|
||||||
"disk": "Disk local",
|
"disk": "Disk local",
|
||||||
"monthly_cost": "Monthly cost",
|
"monthly_cost": "Monthly cost",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
|
"pricing_error": "Couldn't fetch provider prices",
|
||||||
"server_provider": "Server Provider",
|
"server_provider": "Server Provider",
|
||||||
"dns_provider": "DNS Provider",
|
"dns_provider": "DNS Provider",
|
||||||
"core_count": {
|
"core_count": {
|
||||||
|
@ -357,6 +358,9 @@
|
||||||
"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_payment_server": "{} for the server",
|
||||||
|
"choose_server_type_payment_storage": "{} for additional storage",
|
||||||
|
"choose_server_type_payment_ip": "{} for the public IPv4 address",
|
||||||
"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",
|
||||||
|
|
|
@ -124,7 +124,9 @@
|
||||||
"disk": "Диск",
|
"disk": "Диск",
|
||||||
"monthly_cost": "Ежемесячная стоимость",
|
"monthly_cost": "Ежемесячная стоимость",
|
||||||
"location": "Размещение",
|
"location": "Размещение",
|
||||||
"provider": "Провайдер",
|
"server_provider": "Провайдер Сервера",
|
||||||
|
"dns_provider": "Провайдер DNS",
|
||||||
|
"pricing_error": "Не удалось получить цены провайдера",
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} ядро",
|
"one": "{} ядро",
|
||||||
"two": "{} ядра",
|
"two": "{} ядра",
|
||||||
|
@ -337,6 +339,9 @@
|
||||||
"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_payment_server": "{} за сам сервер",
|
||||||
|
"choose_server_type_payment_storage": "{} за расширяемое хранилище",
|
||||||
|
"choose_server_type_payment_ip": "{} за публичный IPv4",
|
||||||
"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 хранилище неверна",
|
||||||
|
|
|
@ -320,7 +320,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();
|
||||||
|
@ -330,7 +330,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,
|
||||||
|
|
|
@ -320,8 +320,8 @@ class HetznerApi extends RestApiMap {
|
||||||
return GenericResult(success: true, data: null);
|
return GenericResult(success: true, data: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<double?>> getPricePerGb() async {
|
Future<GenericResult<HetznerPricing?>> getPricing() async {
|
||||||
double? price;
|
HetznerPricing? pricing;
|
||||||
|
|
||||||
final Response pricingResponse;
|
final Response pricingResponse;
|
||||||
final Dio client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
@ -330,19 +330,34 @@ class HetznerApi extends RestApiMap {
|
||||||
|
|
||||||
final volume = pricingResponse.data['pricing']['volume'];
|
final volume = pricingResponse.data['pricing']['volume'];
|
||||||
final volumePrice = volume['price_per_gb_month']['gross'];
|
final volumePrice = volume['price_per_gb_month']['gross'];
|
||||||
price = double.parse(volumePrice);
|
final primaryIps = pricingResponse.data['pricing']['primary_ips'];
|
||||||
|
String? ipPrice;
|
||||||
|
for (final primaryIp in primaryIps) {
|
||||||
|
if (primaryIp['type'] == 'ipv4') {
|
||||||
|
for (final primaryIpPrice in primaryIp['prices']) {
|
||||||
|
if (primaryIpPrice['location'] == region!) {
|
||||||
|
ipPrice = primaryIpPrice['price_monthly']['gross'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pricing = HetznerPricing(
|
||||||
|
region!,
|
||||||
|
double.parse(volumePrice),
|
||||||
|
double.parse(ipPrice!),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print(e);
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
success: false,
|
success: false,
|
||||||
data: price,
|
data: pricing,
|
||||||
message: e.toString(),
|
message: e.toString(),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return GenericResult(success: true, data: price);
|
return GenericResult(success: true, data: pricing);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<List<HetznerVolume>>> getVolumes({
|
Future<GenericResult<List<HetznerVolume>>> getVolumes({
|
||||||
|
@ -381,7 +396,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();
|
||||||
|
@ -389,7 +404,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,
|
||||||
|
|
|
@ -26,8 +26,17 @@ class ApiProviderVolumeCubit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Price?> getPricePerGb() async =>
|
Future<Price?> getPricePerGb() async {
|
||||||
(await ProvidersController.currentServerProvider!.getPricePerGb()).data;
|
Price? price;
|
||||||
|
final pricingResult =
|
||||||
|
await ProvidersController.currentServerProvider!.getAdditionalPricing();
|
||||||
|
if (pricingResult.data == null || !pricingResult.success) {
|
||||||
|
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
price = pricingResult.data!.perVolumeGb;
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
|
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
|
||||||
|
@ -113,9 +122,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);
|
||||||
|
|
|
@ -7,6 +7,7 @@ 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/rest_maps/backblaze.dart';
|
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.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/backblaze_bucket.dart';
|
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.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';
|
||||||
|
@ -15,6 +16,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
|
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/price.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
import 'package:selfprivacy/logic/models/server_basic_info.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';
|
||||||
|
@ -34,6 +36,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();
|
||||||
|
|
||||||
|
@ -149,6 +153,19 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
return apiResult.data;
|
return apiResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AdditionalPricing?> fetchAvailableAdditionalPricing() async {
|
||||||
|
AdditionalPricing? prices;
|
||||||
|
final pricingResult =
|
||||||
|
await ProvidersController.currentServerProvider!.getAdditionalPricing();
|
||||||
|
if (pricingResult.data == null || !pricingResult.success) {
|
||||||
|
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
prices = pricingResult.data;
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
void setServerProviderKey(final String serverProviderKey) async {
|
void setServerProviderKey(final String serverProviderKey) async {
|
||||||
await repository.saveServerProviderKey(serverProviderKey);
|
await repository.saveServerProviderKey(serverProviderKey);
|
||||||
|
|
||||||
|
@ -169,12 +186,14 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setLocationIdentifier(final String locationId) async {
|
||||||
|
await ProvidersController.currentServerProvider!
|
||||||
|
.trySetServerLocation(locationId);
|
||||||
|
}
|
||||||
|
|
||||||
void setServerType(final ServerType serverType) async {
|
void setServerType(final ServerType serverType) async {
|
||||||
await repository.saveServerType(serverType);
|
await repository.saveServerType(serverType);
|
||||||
|
|
||||||
await ProvidersController.currentServerProvider!
|
|
||||||
.trySetServerLocation(serverType.location.identifier);
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
(state as ServerInstallationNotFinished).copyWith(
|
(state as ServerInstallationNotFinished).copyWith(
|
||||||
serverTypeIdentificator: serverType.identifier,
|
serverTypeIdentificator: serverType.identifier,
|
||||||
|
@ -274,6 +293,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 =
|
||||||
|
|
|
@ -190,3 +190,22 @@ class HetznerVolume {
|
||||||
static HetznerVolume fromJson(final Map<String, dynamic> json) =>
|
static HetznerVolume fromJson(final Map<String, dynamic> json) =>
|
||||||
_$HetznerVolumeFromJson(json);
|
_$HetznerVolumeFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prices for Hetzner resources in Euro (monthly).
|
||||||
|
/// https://docs.hetzner.cloud/#pricing
|
||||||
|
class HetznerPricing {
|
||||||
|
HetznerPricing(
|
||||||
|
this.region,
|
||||||
|
this.perVolumeGb,
|
||||||
|
this.perPublicIpv4,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Region name to which current price listing applies
|
||||||
|
final String region;
|
||||||
|
|
||||||
|
/// The cost of Volume per GB/month
|
||||||
|
final double perVolumeGb;
|
||||||
|
|
||||||
|
/// Costs of Primary IP type
|
||||||
|
final double perPublicIpv4;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,3 +53,12 @@ class Currency {
|
||||||
final String? fontcode;
|
final String? fontcode;
|
||||||
final String? symbol;
|
final String? symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AdditionalPricing {
|
||||||
|
AdditionalPricing({
|
||||||
|
required this.perVolumeGb,
|
||||||
|
required this.perPublicIpv4,
|
||||||
|
});
|
||||||
|
final Price perVolumeGb;
|
||||||
|
final Price perPublicIpv4;
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -527,14 +529,20 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hardcoded on their documentation and there is no pricing API at all
|
|
||||||
/// Probably we should scrap the doc page manually
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<Price?>> getPricePerGb() async => GenericResult(
|
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing() async =>
|
||||||
|
GenericResult(
|
||||||
success: true,
|
success: true,
|
||||||
data: Price(
|
data: AdditionalPricing(
|
||||||
value: 0.10,
|
perVolumeGb: Price(
|
||||||
currency: currency,
|
/// Hardcoded in their documentation and there is no pricing API
|
||||||
|
value: 0.10,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
|
perPublicIpv4: Price(
|
||||||
|
value: 0,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -588,10 +596,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 +716,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 getAdditionalPricing();
|
||||||
|
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;
|
||||||
try {
|
try {
|
||||||
|
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
|
||||||
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 +771,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,
|
||||||
|
|
|
@ -165,7 +165,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(
|
||||||
|
@ -546,8 +548,8 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<Price?>> getPricePerGb() async {
|
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing() async {
|
||||||
final result = await _adapter.api().getPricePerGb();
|
final result = await _adapter.api().getPricing();
|
||||||
|
|
||||||
if (!result.success || result.data == null) {
|
if (!result.success || result.data == null) {
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
|
@ -560,9 +562,15 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
|
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
success: true,
|
success: true,
|
||||||
data: Price(
|
data: AdditionalPricing(
|
||||||
value: result.data!,
|
perVolumeGb: Price(
|
||||||
currency: currency,
|
value: result.data!.perVolumeGb,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
|
perPublicIpv4: Price(
|
||||||
|
value: result.data!.perPublicIpv4,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -617,10 +625,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(
|
||||||
|
@ -705,21 +713,46 @@ 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 getAdditionalPricing();
|
||||||
|
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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
|
||||||
|
final Price pricePerIp = resultPricePerGb.data!.perPublicIpv4;
|
||||||
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(
|
||||||
|
@ -746,7 +779,8 @@ 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}',
|
// TODO: Make more descriptive
|
||||||
|
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} + ${(volume.size * pricePerGb.value).toStringAsFixed(2)} + ${pricePerIp.value.toStringAsFixed(2)} ${currency.shortcode}',
|
||||||
),
|
),
|
||||||
ServerMetadataEntity(
|
ServerMetadataEntity(
|
||||||
type: MetadataType.location,
|
type: MetadataType.location,
|
||||||
|
|
|
@ -90,9 +90,9 @@ abstract class ServerProvider {
|
||||||
/// answered the request.
|
/// answered the request.
|
||||||
Future<GenericResult<DateTime?>> restart(final int serverId);
|
Future<GenericResult<DateTime?>> restart(final int serverId);
|
||||||
|
|
||||||
/// Returns [Price] information per one gigabyte of storage extension for
|
/// Returns [Price] information map of all additional resources, excluding
|
||||||
/// the requested accessible machine.
|
/// main server type pricing
|
||||||
Future<GenericResult<Price?>> getPricePerGb();
|
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing();
|
||||||
|
|
||||||
/// Returns [ServerVolume] of all available volumes
|
/// Returns [ServerVolume] of all available volumes
|
||||||
/// assigned to the authorized user and attached to active machine.
|
/// assigned to the authorized user and attached to active machine.
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -46,6 +46,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
|
||||||
late double _currentSliderGbValue;
|
late double _currentSliderGbValue;
|
||||||
double _pricePerGb = 1.0;
|
double _pricePerGb = 1.0;
|
||||||
|
|
||||||
|
// TODO: Wtfff hardcode?!?!?
|
||||||
final DiskSize maxSize = const DiskSize(byte: 500000000000);
|
final DiskSize maxSize = const DiskSize(byte: 500000000000);
|
||||||
late DiskSize minSize;
|
late DiskSize minSize;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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/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';
|
||||||
|
@ -25,7 +26,12 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
|
||||||
ServerProviderLocation? serverProviderLocation;
|
ServerProviderLocation? serverProviderLocation;
|
||||||
ServerType? serverType;
|
ServerType? serverType;
|
||||||
|
|
||||||
void setServerProviderLocation(final ServerProviderLocation? location) {
|
void setServerProviderLocation(final ServerProviderLocation? location) async {
|
||||||
|
if (location != null) {
|
||||||
|
await widget.serverInstallationCubit.setLocationIdentifier(
|
||||||
|
location.identifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
serverProviderLocation = location;
|
serverProviderLocation = location;
|
||||||
});
|
});
|
||||||
|
@ -153,194 +159,320 @@ class SelectTypePage extends StatelessWidget {
|
||||||
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<AdditionalPricing?> prices =
|
||||||
final AsyncSnapshot<Object?> snapshot,
|
serverInstallationCubit.fetchAvailableAdditionalPricing();
|
||||||
) {
|
return FutureBuilder(
|
||||||
if (snapshot.hasData) {
|
future: Future.wait([
|
||||||
if ((snapshot.data as List<ServerType>).isEmpty) {
|
serverTypes,
|
||||||
return Column(
|
prices,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
]),
|
||||||
children: [
|
builder: (
|
||||||
Text(
|
final BuildContext context,
|
||||||
'initializing.locations_not_found'.tr(),
|
final AsyncSnapshot<List<dynamic>> snapshot,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
) {
|
||||||
),
|
if (snapshot.hasData) {
|
||||||
const SizedBox(height: 16),
|
if ((snapshot.data![0] as List<ServerType>).isEmpty ||
|
||||||
Text(
|
(snapshot.data![1] as AdditionalPricing?) == null) {
|
||||||
'initializing.locations_not_found_text'.tr(),
|
return Column(
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
LayoutBuilder(
|
Text(
|
||||||
builder: (final context, final constraints) => CustomPaint(
|
'initializing.locations_not_found'.tr(),
|
||||||
size: Size(
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
constraints.maxWidth,
|
),
|
||||||
(constraints.maxWidth * 1).toDouble(),
|
const SizedBox(height: 16),
|
||||||
),
|
Text(
|
||||||
painter: StrayDeerPainter(
|
'initializing.locations_not_found_text'.tr(),
|
||||||
colorScheme: Theme.of(context).colorScheme,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
colorPalette: context
|
),
|
||||||
.read<AppSettingsCubit>()
|
LayoutBuilder(
|
||||||
.state
|
builder: (final context, final constraints) => CustomPaint(
|
||||||
.corePaletteOrDefault,
|
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(
|
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(
|
final prices = snapshot.data![1] as AdditionalPricing;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final storagePrice = serverInstallationCubit.initialStorage.gibibyte *
|
||||||
children: [
|
prices.perVolumeGb.value;
|
||||||
Text(
|
final publicIpPrice = prices.perPublicIpv4.value;
|
||||||
'initializing.choose_server_type'.tr(),
|
return ResponsiveLayoutWithInfobox(
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
topChild: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'initializing.choose_server_type_text'.tr(),
|
'initializing.choose_server_type'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 16),
|
||||||
),
|
Text(
|
||||||
primaryColumn: Column(
|
'initializing.choose_server_type_text'.tr(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
children: [
|
),
|
||||||
...(snapshot.data! as List<ServerType>).map(
|
],
|
||||||
(final type) => Column(
|
),
|
||||||
children: [
|
primaryColumn: Column(
|
||||||
SizedBox(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
width: double.infinity,
|
children: [
|
||||||
child: InkWell(
|
...(snapshot.data![0] as List<ServerType>).map(
|
||||||
onTap: () {
|
(final type) => Column(
|
||||||
serverInstallationCubit.setServerType(type);
|
children: [
|
||||||
},
|
SizedBox(
|
||||||
child: Card(
|
width: double.infinity,
|
||||||
child: Padding(
|
child: InkWell(
|
||||||
padding: const EdgeInsets.all(16.0),
|
onTap: () {
|
||||||
child: Column(
|
serverInstallationCubit.setServerType(type);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
},
|
||||||
children: [
|
child: Card(
|
||||||
Text(
|
child: Padding(
|
||||||
type.title,
|
padding: const EdgeInsets.all(16.0),
|
||||||
style: Theme.of(context)
|
child: Column(
|
||||||
.textTheme
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
.titleMedium,
|
children: [
|
||||||
),
|
Text(
|
||||||
const SizedBox(height: 8),
|
type.title,
|
||||||
Row(
|
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()],
|
||||||
|
),
|
||||||
|
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 + storagePrice + publicIpPrice).toStringAsFixed(4)} ${type.price.currency.shortcode}'
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
VerticalDivider(
|
||||||
Icons.memory_outlined,
|
width: 24.0,
|
||||||
|
indent: 4.0,
|
||||||
|
endIndent: 4.0,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSurface,
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Column(
|
||||||
'server.core_count'
|
crossAxisAlignment:
|
||||||
.plural(type.cores),
|
CrossAxisAlignment.start,
|
||||||
style: Theme.of(context)
|
children: [
|
||||||
.textTheme
|
Row(
|
||||||
.bodyMedium,
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.memory_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_server'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
type.price.value
|
||||||
|
.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.sd_card_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_storage'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
storagePrice.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (publicIpPrice != 0)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.lan_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_ip'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
publicIpPrice.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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());
|
||||||
},
|
}
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue