Browse Source

update

master
Kherel 1 year ago
parent
commit
804147b8d6
  1. 19
      assets/translations/en.json
  2. 17
      assets/translations/ru.json
  3. 7
      build.yaml
  4. 6
      lib/config/text_themes.dart
  5. 26
      lib/logic/api_maps/api_map.dart
  6. 4
      lib/logic/api_maps/backblaze.dart
  7. 22
      lib/logic/api_maps/cloudflare.dart
  8. 21
      lib/logic/api_maps/hetzner.dart
  9. 3
      lib/logic/api_maps/server.dart
  10. 2
      lib/logic/cubit/app_config/app_config_cubit.dart
  11. 1
      lib/logic/cubit/app_config/app_config_state.dart
  12. 24
      lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart
  13. 9
      lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart
  14. 29
      lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart
  15. 6
      lib/logic/models/backblaze_credential.dart
  16. 4
      lib/logic/models/backblaze_credential.g.dart
  17. 9
      lib/logic/models/cloudflare_domain.dart
  18. 4
      lib/logic/models/cloudflare_domain.g.dart
  19. 89
      lib/logic/models/hetzner_server_info.dart
  20. 84
      lib/logic/models/hetzner_server_info.g.dart
  21. 23
      lib/logic/models/server_info.dart
  22. 2
      lib/main.dart
  23. 2
      lib/ui/components/brand_button/brand_button.dart
  24. 26
      lib/ui/components/brand_text/brand_text.dart
  25. 51
      lib/ui/components/one_page/one_page.dart
  26. 30
      lib/ui/components/pre_styled_buttons.dart
  27. 116
      lib/ui/pages/providers/providers.dart
  28. 290
      lib/ui/pages/server_details/server_details.dart
  29. 136
      lib/ui/pages/server_details/server_settings.dart
  30. 346
      lib/ui/pages/services/services.dart
  31. 6
      lib/ui/pages/users/new_user.dart
  32. 26
      lib/ui/pages/users/user_details.dart
  33. 1
      lib/ui/pages/users/users.dart
  34. 4
      lib/utils/route_transitions/slide_bottom.dart
  35. 9
      lib/utils/ui_helpers.dart
  36. 14
      pubspec.lock
  37. 2
      pubspec.yaml

19
assets/translations/en.json

@ -20,9 +20,11 @@
"domain": "Domain",
"saving": "Saving..",
"nickname": "nickname",
"loading": "loading",
"loading": "Loading...",
"later": "Настрою потом",
"reset": "Reset"
"reset": "Reset",
"details": "Details",
"no_data": "No data"
},
"more": {
"_comment": "'More' tab",
@ -54,26 +56,25 @@
"page_title": "Your Data Center",
"server": {
"card_title": "Server",
"status": "Status — Good",
"bottom_sheet": {
"1": "It's a virtual computer, where all your services live.",
"2": "1 CPU, RAM 4Gb, 40Gb — $5 per month",
"3": "Status — Good"
"1": "It's a virtual computer, where all your services live."
}
},
"domain": {
"card_title": "Domain",
"status": "Status — Good",
"bottom_sheet": {
"1": "It's your personal internet address that will point to the server and other services of yours.",
"2": "{} — expires on {}",
"3": "Status — Good"
"2": "{} — expires on {}"
}
},
"backup": {
"card_title": "Backup",
"status": "Status — Good",
"bottom_sheet": {
"1": "Will save your day in case of incident: hackers attack, server deletion, etc.",
"2": "3Gb/10Gb, last backup was yesterday {}",
"3": "Status — Good"
"2": "3Gb/10Gb, last backup was yesterday {}"
}
}
},

17
assets/translations/ru.json

@ -22,7 +22,9 @@
"nickname": "Никнейм",
"loading": "Загрузка",
"later": "Настрою потом",
"reset": "Reset"
"reset": "Reset",
"details": "Детальная информация",
"no_data": "Нет данных"
},
"more": {
"_comment": "вкладка еще",
@ -54,26 +56,25 @@
"page_title": "Ваш Дата-центр",
"server": {
"card_title": "Сервер",
"status": "Статус — в норме",
"bottom_sheet": {
"1": "Это виртульный компьютер на котором работают все ваши сервисы.",
"2": "1 CPU, RAM 4Gb, 40Gb — $5 в месяц",
"3": "Статус — в норме"
"1": "Это виртульный компьютер на котором работают все ваши сервисы."
}
},
"domain": {
"card_title": "Домен",
"status": "Статус — в норме",
"bottom_sheet": {
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы.",
"2": "{} — продлен до {}",
"3": "Статус — в норме"
"2": "{} — продлен до {}"
}
},
"backup": {
"card_title": "Резервное копирование",
"status": "Статус — в норме",
"bottom_sheet": {
"1": "Выручит в любой ситуации: хакерская атака, удаление сервера и т.п.",
"2": "3Gb — бестплатно до 10Gb, последний вчера в {}",
"3": "Статус — в норме"
"2": "3Gb — бестплатно до 10Gb, последний вчера в {}"
}
}
},

7
build.yaml

@ -0,0 +1,7 @@
targets:
$default:
builders:
json_serializable:
options:
create_factory: true
create_to_json: false

6
lib/config/text_themes.dart

@ -38,6 +38,12 @@ final headline4Style = defaultTextStyle.copyWith(
color: BrandColors.headlineColor,
);
final headline5Style = defaultTextStyle.copyWith(
fontSize: 15,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor.withOpacity(0.8),
);
final body1Style = defaultTextStyle;
final body2Style = defaultTextStyle.copyWith(
color: BrandColors.textColor2,

26
lib/logic/api_maps/api_map.dart

@ -10,7 +10,6 @@ import 'package:selfprivacy/logic/get_it/console.dart';
import 'package:selfprivacy/logic/models/message.dart';
abstract class ApiMap {
Future<Dio> getClient() async {
var dio = Dio(await options);
if (hasLoger) {
@ -31,27 +30,14 @@ abstract class ApiMap {
abstract final String rootAddress;
abstract final bool hasLoger;
abstract final bool isWithToken;
}
// abstract class ApiMapOld {
// ApiMapOld() {
// var client = Dio()..interceptors.add(ConsoleInterceptor());
// (client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
// (HttpClient client) {
// client.badCertificateCallback =
// (X509Certificate cert, String host, int port) => true;
// return client;
// };
// loggedClient = client;
// }
// String? rootAddress;
// late Dio loggedClient;
ValidateStatus? validateStatus;
// void close() {
// loggedClient.close();
// }
// }
void close(Dio client) {
client.close();
validateStatus = null;
}
}
class ConsoleInterceptor extends InterceptorsWrapper {
void addMessage(Message message) {

4
lib/logic/api_maps/backblaze.dart

@ -11,7 +11,6 @@ class BackblazeApi extends ApiMap {
if (isWithToken) {
var backblazeCredential = getIt<ApiConfigModel>().backblazeCredential;
var token = backblazeCredential!.applicationKey;
assert(token != null);
options.headers = {'Authorization': 'Basic $token'};
}
@ -22,7 +21,6 @@ class BackblazeApi extends ApiMap {
return options;
}
ValidateStatus? validateStatus;
@override
String rootAddress = 'https://api.backblazeb2.com/b2api/v2/';
@ -32,7 +30,7 @@ class BackblazeApi extends ApiMap {
'b2_authorize_account',
options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
);
client.close();
close(client);
if (response.statusCode == HttpStatus.ok) {
return true;
} else if (response.statusCode == HttpStatus.unauthorized) {

22
lib/logic/api_maps/cloudflare.dart

@ -22,8 +22,6 @@ class CloudflareApi extends ApiMap {
return options;
}
ValidateStatus? validateStatus;
@override
String rootAddress = 'https://api.cloudflare.com/client/v4';
@ -36,8 +34,7 @@ class CloudflareApi extends ApiMap {
Response response = await client.get('/user/tokens/verify',
options: Options(headers: {'Authorization': 'Bearer $token'}));
client.close();
validateStatus = null;
close(client);
if (response.statusCode == HttpStatus.ok) {
return true;
@ -48,7 +45,7 @@ class CloudflareApi extends ApiMap {
}
}
Future<String?> getZoneId(String domain) async {
Future<String> getZoneId(String domain) async {
validateStatus = (status) {
return status == HttpStatus.ok || status == HttpStatus.forbidden;
};
@ -58,14 +55,9 @@ class CloudflareApi extends ApiMap {
queryParameters: {'name': domain},
);
client.close();
validateStatus = null;
close(client);
try {
return response.data['result'][0]['id'];
} catch (error) {
return null;
}
return response.data['result'][0]['id'];
}
Future<void> removeSimilarRecords({
@ -92,7 +84,7 @@ class CloudflareApi extends ApiMap {
}
await Future.wait(allDeleteFutures);
client.close();
close(client);
}
Future<void> createMultipleDnsRecords({
@ -118,7 +110,7 @@ class CloudflareApi extends ApiMap {
}
await Future.wait(allCreateFutures);
client.close();
close(client);
}
List<DnsRecords> projectDnsRecords(String? domainName, String? ip4) {
@ -171,7 +163,7 @@ class CloudflareApi extends ApiMap {
queryParameters: {'per_page': 50},
);
client.close();
close(client);
return response.data['result']
.map<String>((el) => el['name'] as String)
.toList();

21
lib/logic/api_maps/hetzner.dart

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/server_details.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'package:selfprivacy/utils/password_generator2.dart';
@ -29,8 +30,6 @@ class HetznerApi extends ApiMap {
return options;
}
ValidateStatus? validateStatus;
@override
String rootAddress = 'https://api.hetzner.cloud/v1';
@ -45,7 +44,7 @@ class HetznerApi extends ApiMap {
headers: {'Authorization': 'Bearer $token'},
),
);
client.close();
close(client);
if (response.statusCode == HttpStatus.ok) {
return true;
@ -87,7 +86,7 @@ class HetznerApi extends ApiMap {
List list = response.data['servers'];
var server = list.firstWhere((el) => el['name'] == 'selfprivacy-server');
await client.delete('/servers/${server['id']}');
client.close();
close(client);
}
Future<HetznerServerDetails> startServer({
@ -96,7 +95,7 @@ class HetznerApi extends ApiMap {
var client = await getClient();
await client.post('/servers/${server.id}/actions/poweron');
client.close();
close(client);
return server.copyWith(
startTime: DateTime.now(),
@ -108,7 +107,7 @@ class HetznerApi extends ApiMap {
}) async {
var client = await getClient();
await client.post('/servers/${server.id}/actions/poweron');
client.close();
close(client);
return server.copyWith(
startTime: DateTime.now(),
);
@ -118,13 +117,15 @@ class HetznerApi extends ApiMap {
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
var client = await getClient();
await client.post('/servers/${hetznerServer!.id}/metrics');
client.close();
close(client);
}
getInfo() async {
Future<HetznerServerInfo> getInfo() async {
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
var client = await getClient();
await client.post('/servers/${hetznerServer!.id}');
client.close();
Response response = await client.get('/servers/${hetznerServer!.id}');
close(client);
return HetznerServerInfo.fromJson(response.data!['server']);
}
}

3
lib/logic/api_maps/server.dart

@ -18,7 +18,6 @@ class ServerApi extends ApiMap {
if (isWithToken) {
var cloudFlareDomain = getIt<ApiConfigModel>().cloudFlareDomain;
var domainName = cloudFlareDomain!.domainName;
assert(domainName != null);
options = BaseOptions(baseUrl: 'https://api.$domainName');
}
@ -37,7 +36,7 @@ class ServerApi extends ApiMap {
} catch (e) {
res = false;
}
client.close();
close(client);
return res;
}

2
lib/logic/cubit/app_config/app_config_cubit.dart

@ -257,7 +257,7 @@ class AppConfigCubit extends Cubit<AppConfigState> {
emit(state.copyWith(isLoading: true));
await repository.createServer(
state.rootUser!,
state.cloudFlareDomain!.domainName!,
state.cloudFlareDomain!.domainName,
state.cloudFlareKey!,
onCancel: onCancel,
onSuccess: onSuccess,

1
lib/logic/cubit/app_config/app_config_state.dart

@ -92,7 +92,6 @@ class AppConfigState extends Equatable {
isServerReseted,
hasFinalChecked,
];
print('progress: $res');
return res;
}
}

24
lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart

@ -0,0 +1,24 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart';
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
part 'server_detailed_info_state.dart';
class ServerDetailsCubit extends Cubit<ServerDetailsState> {
ServerDetailsCubit() : super(ServerDetailsInitial());
ServerDetailsRepository repository = ServerDetailsRepository();
void check() async {
var isReadyToCheck = getIt<ApiConfigModel>().hetznerServer != null;
if (isReadyToCheck) {
emit(ServerDetailsLoading());
var data = await repository.load();
emit(Loaded(serverInfo: data, checkTime: DateTime.now()));
} else {
emit(ServerDetailsNotReady());
}
}
}

9
lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart

@ -0,0 +1,9 @@
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
class ServerDetailsRepository {
Future<HetznerServerInfo> load() async {
var client = HetznerApi();
return await client.getInfo();
}
}

29
lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart

@ -0,0 +1,29 @@
part of 'server_detailed_info_cubit.dart';
abstract class ServerDetailsState extends Equatable {
const ServerDetailsState();
@override
List<Object> get props => [];
}
class ServerDetailsInitial extends ServerDetailsState {}
class ServerDetailsLoading extends ServerDetailsState {}
class ServerDetailsNotReady extends ServerDetailsState {}
class Loading extends ServerDetailsState {}
class Loaded extends ServerDetailsState {
final HetznerServerInfo serverInfo;
final DateTime checkTime;
Loaded({
required this.serverInfo,
required this.checkTime,
});
@override
List<Object> get props => [serverInfo, checkTime];
}

6
lib/logic/models/backblaze_credential.dart

@ -6,13 +6,13 @@ part 'backblaze_credential.g.dart';
@HiveType(typeId: 4)
class BackblazeCredential {
BackblazeCredential({this.keyId, this.applicationKey});
BackblazeCredential({required this.keyId, required this.applicationKey});
@HiveField(0)
final String? keyId;
final String keyId;
@HiveField(1)
final String? applicationKey;
final String applicationKey;
get encodedApiKey => encodedBackblazeKey(keyId, applicationKey);

4
lib/logic/models/backblaze_credential.g.dart

@ -17,8 +17,8 @@ class BackblazeCredentialAdapter extends TypeAdapter<BackblazeCredential> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return BackblazeCredential(
keyId: fields[0] as String?,
applicationKey: fields[1] as String?,
keyId: fields[0] as String,
applicationKey: fields[1] as String,
);
}

9
lib/logic/models/cloudflare_domain.dart

@ -4,13 +4,16 @@ part 'cloudflare_domain.g.dart';
@HiveType(typeId: 3)
class CloudFlareDomain {
CloudFlareDomain({this.domainName, this.zoneId});
CloudFlareDomain({
required this.domainName,
required this.zoneId,
});
@HiveField(0)
final String? domainName;
final String domainName;
@HiveField(1)
final String? zoneId;
final String zoneId;
@override
String toString() {

4
lib/logic/models/cloudflare_domain.g.dart

@ -17,8 +17,8 @@ class CloudFlareDomainAdapter extends TypeAdapter<CloudFlareDomain> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CloudFlareDomain(
domainName: fields[0] as String?,
zoneId: fields[1] as String?,
domainName: fields[0] as String,
zoneId: fields[1] as String,
);
}

89
lib/logic/models/hetzner_server_info.dart

@ -0,0 +1,89 @@
import 'package:json_annotation/json_annotation.dart';
part 'hetzner_server_info.g.dart';
@JsonSerializable()
class HetznerServerInfo {
final int id;
final String name;
final ServerStatus status;
final DateTime created;
@JsonKey(name: 'server_type')
final HetznerServerTypeInfo serverType;
@JsonKey(name: 'datacenter', fromJson: HetznerServerInfo.locationFromJson)
final HetznerLocation location;
static HetznerLocation locationFromJson(Map json) =>
HetznerLocation.fromJson(json['location']);
static HetznerServerInfo fromJson(Map<String, dynamic> json) =>
_$HetznerServerInfoFromJson(json);
HetznerServerInfo(
this.id,
this.name,
this.status,
this.created,
this.serverType,
this.location,
);
}
enum ServerStatus {
running,
initializing,
starting,
stopping,
off,
deleting,
migrating,
rebuilding,
unknown,
}
@JsonSerializable()
class HetznerServerTypeInfo {
final int cores;
final num memory;
final int disk;
final List<HetznerPriceInfo> prices;
HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices);
static HetznerServerTypeInfo fromJson(Map<String, dynamic> json) =>
_$HetznerServerTypeInfoFromJson(json);
}
@JsonSerializable()
class HetznerPriceInfo {
HetznerPriceInfo(this.hourly, this.monthly);
@JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice)
final double hourly;
@JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice)
final double monthly;
static HetznerPriceInfo fromJson(Map<String, dynamic> json) =>
_$HetznerPriceInfoFromJson(json);
static double getPrice(Map json) => double.parse(json['gross'] as String);
}
@JsonSerializable()
class HetznerLocation {
final String country;
final String city;
final String description;
@JsonKey(name: 'network_zone')
final String zone;
HetznerLocation(this.country, this.city, this.description, this.zone);
static HetznerLocation fromJson(Map<String, dynamic> json) =>
_$HetznerLocationFromJson(json);
}

84
lib/logic/models/hetzner_server_info.g.dart

@ -0,0 +1,84 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hetzner_server_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) {
return HetznerServerInfo(
json['id'] as int,
json['name'] as String,
_$enumDecode(_$ServerStatusEnumMap, json['status']),
DateTime.parse(json['created'] as String),
HetznerServerTypeInfo.fromJson(json['server_type'] as Map<String, dynamic>),
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
);
}
K _$enumDecode<K, V>(
Map<K, V> enumValues,
Object? source, {
K? unknownValue,
}) {
if (source == null) {
throw ArgumentError(
'A value must be provided. Supported values: '
'${enumValues.values.join(', ')}',
);
}
return enumValues.entries.singleWhere(
(e) => e.value == source,
orElse: () {
if (unknownValue == null) {
throw ArgumentError(
'`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}',
);
}
return MapEntry(unknownValue, enumValues.values.first);
},
).key;
}
const _$ServerStatusEnumMap = {
ServerStatus.running: 'running',
ServerStatus.initializing: 'initializing',
ServerStatus.starting: 'starting',
ServerStatus.stopping: 'stopping',
ServerStatus.off: 'off',
ServerStatus.deleting: 'deleting',
ServerStatus.migrating: 'migrating',
ServerStatus.rebuilding: 'rebuilding',
ServerStatus.unknown: 'unknown',
};
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
Map<String, dynamic> json) {
return HetznerServerTypeInfo(
json['cores'] as int,
json['memory'] as num,
json['disk'] as int,
(json['prices'] as List<dynamic>)
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
HetznerPriceInfo _$HetznerPriceInfoFromJson(Map<String, dynamic> json) {
return HetznerPriceInfo(
HetznerPriceInfo.getPrice(json['price_hourly'] as Map),
HetznerPriceInfo.getPrice(json['price_monthly'] as Map),
);
}
HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) {
return HetznerLocation(
json['country'] as String,
json['city'] as String,
json['description'] as String,
json['network_zone'] as String,
);
}

23
lib/logic/models/server_info.dart

@ -1,23 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
@JsonSerializable(createFactory: false)
class ServerInfo {
final String id;
final String name;
final ServerStatus status;
final DateTime created;
ServerInfo(this.id, this.name, this.status, this.created);
}
enum ServerStatus {
running,
initializing,
starting,
stopping,
off,
deleting,
migrating,
rebuilding,
unknown,
}

2
lib/main.dart

@ -37,8 +37,6 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
AppSettingsState appSettings = context.watch<AppSettingsCubit>().state;
var a = DateTime.parse('2021-03-23T20:00:06+00:00');
print(a);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, // Manually changnig appbar color
child: MaterialApp(

2
lib/ui/components/brand_button/brand_button.dart

@ -34,7 +34,7 @@ class BrandButton {
onPressed: onPressed,
);
static iconText({
static emptyWithIconText({
Key? key,
required VoidCallback onPressed,
required String title,

26
lib/ui/components/brand_text/brand_text.dart

@ -6,6 +6,7 @@ enum TextType {
h2, // cards titles
h3, // titles in about page
h4, // caption
h5, // Table data
body1, // normal
body2, // with opacity
medium,
@ -63,10 +64,28 @@ class BrandText extends StatelessWidget {
textAlign: textAlign,
overflow: TextOverflow.ellipsis,
);
factory BrandText.h4(String? text, {TextStyle? style}) => BrandText(
factory BrandText.h4(
String? text, {
TextStyle? style,
TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h4,
style: style,
textAlign: textAlign,
);
factory BrandText.h5(
String? text, {
TextStyle? style,
TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h5,
style: style,
textAlign: textAlign,
);
factory BrandText.body1(String? text, {TextStyle? style}) => BrandText(
text,
@ -123,6 +142,11 @@ class BrandText extends StatelessWidget {
? headline4Style.copyWith(color: Colors.white)
: headline4Style;
break;
case TextType.h5:
style = isDark
? headline5Style.copyWith(color: Colors.white)
: headline5Style;
break;
case TextType.body1:
style = isDark ? body1Style.copyWith(color: Colors.white) : body1Style;
break;

51
lib/ui/components/one_page/one_page.dart

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons.dart';
class OnePage extends StatelessWidget {
const OnePage({
Key? key,
required this.title,
required this.child,
}) : super(key: key);
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PreferredSize(
child: Column(
children: [
Container(
height: 51,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15),
child: BrandText.h4('basis.details'.tr()),
),
BrandDivider(),
],
),
preferredSize: Size.fromHeight(52),
),
body: child,
bottomNavigationBar: SafeArea(
child: Container(
decoration: BoxDecoration(boxShadow: kElevationToShadow[3]),
height: kBottomNavigationBarHeight,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
alignment: Alignment.center,
child: PreStyledButtons.close(
onPress: () => Navigator.of(context).pop()),
),
),
),
),
);
}
}

30
lib/ui/components/pre_styled_buttons.dart

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:easy_localization/easy_localization.dart';
class PreStyledButtons {
static Widget close({
required VoidCallback onPress,
}) =>
_CloseButton(onPress: onPress);
}
class _CloseButton extends StatelessWidget {
const _CloseButton({Key? key, required this.onPress}) : super(key: key);
final VoidCallback onPress;
@override
Widget build(BuildContext context) {
return OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
BrandText.h4('basis.close'.tr()),
Icon(Icons.close),
],
),
);
}
}

116
lib/ui/pages/providers/providers.dart

@ -10,9 +10,15 @@ import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.da
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:selfprivacy/ui/components/one_page/one_page.dart';
import 'package:selfprivacy/ui/pages/providers/settings/settings.dart';
import 'package:selfprivacy/ui/pages/server_details/server_details.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/utils/route_transitions/slide_bottom.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
var navigatorKey = GlobalKey<NavigatorState>();
class ProvidersPage extends StatefulWidget {
ProvidersPage({Key? key}) : super(key: key);
@ -64,9 +70,11 @@ class _Card extends StatelessWidget {
final ProviderModel provider;
@override
Widget build(BuildContext context) {
String? title;
late String title;
String? message;
String? stableText;
late String stableText;
late VoidCallback onTap;
AppConfigState appConfig = context.watch<AppConfigCubit>().state;
var domainName =
@ -75,30 +83,54 @@ class _Card extends StatelessWidget {
switch (provider.type) {
case ProviderType.server:
title = 'providers.server.card_title'.tr();
stableText = 'В норме';
stableText = 'providers.domain.status'.tr();
stableText = 'providers.server.status'.tr();
onTap = () => Navigator.of(context).push(
SlideBottomRoute(
OnePage(
title: title,
child: ServerDetails(),
),
),
);
break;
case ProviderType.domain:
title = 'providers.domain.card_title'.tr();
message = domainName;
stableText = 'Домен настроен';
stableText = 'providers.domain.status'.tr();
onTap = () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ProviderDetails(
provider: provider,
statusText: stableText,
);
},
);
break;
case ProviderType.backup:
title = 'providers.backup.card_title'.tr();
stableText = 'В норме';
stableText = 'providers.backup.status'.tr();
onTap = () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ProviderDetails(
provider: provider,
statusText: stableText,
);
},
);
break;
}
return GestureDetector(
onTap: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ProviderDetails(
provider: provider,
statusText: stableText,
);
},
),
onTap: onTap,
child: BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -139,20 +171,11 @@ class _ProviderDetails extends StatelessWidget {
var config = context.watch<AppConfigCubit>().state;
var domainName = config.isDomainFilled
? config.cloudFlareDomain!.domainName!
: 'example.com';
var domainName = UiHelpers.getDomainName(config);
switch (provider.type) {
case ProviderType.server:
title = 'providers.server.card_title'.tr();
children = [
BrandText.body1('providers.server.bottom_sheet.1'.tr()),
SizedBox(height: 10),
BrandText.body1('providers.server.bottom_sheet.2'.tr()),
SizedBox(height: 10),
BrandText.body1('providers.server.bottom_sheet.3'.tr()),
];
break;
throw ('wrong type');
case ProviderType.domain:
title = 'providers.domain.card_title'.tr();
children = [
@ -161,7 +184,7 @@ class _ProviderDetails extends StatelessWidget {
BrandText.body1(
'providers.domain.bottom_sheet.2'.tr(args: [domainName, 'Date'])),
SizedBox(height: 10),
BrandText.body1('providers.domain.bottom_sheet.3'.tr()),
BrandText.body1('providers.domain.status'.tr()),
];
break;
case ProviderType.backup:
@ -185,38 +208,7 @@ class _ProviderDetails extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 2,
),
child: PopupMenuButton<_PopupMenuItemType>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
onSelected: (_PopupMenuItemType result) {
switch (result) {
case _PopupMenuItemType.setting:
navigatorKey.currentState!
.push(materialRoute(SettingsPage()));
break;
}
},
icon: Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => [
PopupMenuItem<_PopupMenuItemType>(
value: _PopupMenuItemType.setting,
child: Container(
padding: EdgeInsets.only(left: 5),
child: Text('basis.settings'.tr()),
),
),
],
),
),
),
SizedBox(height: 40),
Padding(
padding: brandPagePadding2,
child: Column(
@ -242,5 +234,3 @@ class _ProviderDetails extends StatelessWidget {
);
}
}
enum _PopupMenuItemType { setting }

290
lib/ui/pages/server_details/server_details.dart

@ -0,0 +1,290 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/components/switch_block/switch_bloc.dart';
import 'package:selfprivacy/utils/named_font_weight.dart';
part 'server_settings.dart';
var navigatorKey = GlobalKey<NavigatorState>();
class ServerDetails extends StatefulWidget {
const ServerDetails({Key? key}) : super(key: key);
@override
_ServerDetailsState createState() => _ServerDetailsState();
}
class _ServerDetailsState extends State<ServerDetails>
with SingleTickerProviderStateMixin {
late TabController tabController;
@override
void dispose() {
tabController.dispose();
super.dispose();
}
@override
void initState() {
tabController = TabController(length: 2, vsync: this);
tabController.addListener(() {
setState(() {});
});
super.initState();
}
@override
Widget build(BuildContext context) {
var isReady = context.watch<AppConfigCubit>().state.isFullyInitilized;
var providerState = isReady ? StateType.stable : StateType.uninitialized;
late String title = 'providers.server.card_title'.tr();
return TabBarView(
physics: NeverScrollableScrollPhysics(),
controller: tabController,
children: [
BlocProvider(
create: (context) => ServerDetailsCubit()..check(),
child: Builder(builder: (context) {
var details = context.watch<ServerDetailsCubit>().state;
if (details is ServerDetailsLoading ||
details is ServerDetailsInitial) {
return _TempMessage(message: 'basis.loading'.tr());
} else if (details is ServerDetailsNotReady) {
return _TempMessage(message: 'basis.no_data'.tr());
} else if (details is Loaded) {
var data = details.serverInfo;
var checkTime = details.checkTime;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: brandPagePadding2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconStatusMask(
status: providerState,
child: Icon(
BrandIcons.server,
size: 40,
color: Colors.white,
),
),
SizedBox(width: 10),
BrandText.h2(title),
Spacer(),
Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 2,
),
child: PopupMenuButton<_PopupMenuItemType>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
onSelected: (_PopupMenuItemType result) {
switch (result) {
case _PopupMenuItemType.setting:
tabController.animateTo(1);
break;
}
},
icon: Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => [
PopupMenuItem<_PopupMenuItemType>(
value: _PopupMenuItemType.setting,
child: Container(
padding: EdgeInsets.only(left: 5),
child: Text('basis.settings'.tr()),
),
),
],
),
),
],
),
SizedBox(height: 10),
BrandText.body1('providers.server.bottom_sheet.1'.tr()),
SizedBox(height: 30),
Center(child: BrandText.h2('General information')),
SizedBox(height: 10),
Table(
columnWidths: {
0: FractionColumnWidth(.5),
1: FractionColumnWidth(.5),
},
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: [
TableRow(
children: [
getRowTitle('Last check'),
getRowValue(formater.format(checkTime)),
],
),
TableRow(
children: [
getRowTitle('Server Id'),
getRowValue(data.id.toString()),
],
),
TableRow(
children: [
getRowTitle('Status:'),
getRowValue(
'${data.status.toString().split('.')[1].toUpperCase()}',
isBold: true,
),
],
),
TableRow(
children: [
getRowTitle('CPU'),
getRowValue(
data.serverType.cores.toString(),
),
],
),
TableRow(
children: [
getRowTitle('Memory'),
getRowValue(
'${data.serverType.memory.toString()} GB',
),
],
),
TableRow(
children: [
getRowTitle('Disk Local'),
getRowValue(
'${data.serverType.disk.toString()} GB',
),
],
),
TableRow(
children: [
getRowTitle('Price monthly:'),
getRowValue(
'${data.serverType.prices[1].monthly.toString()}',
),
],
),
TableRow(
children: [
getRowTitle('Price hourly:'),
getRowValue(
'${data.serverType.prices[1].hourly.toString()}',
),
],
),
],
),
SizedBox(height: 30),
Center(child: BrandText.h2('Location')),
SizedBox(height: 10),
Table(
columnWidths: {
0: FractionColumnWidth(.5),
1: FractionColumnWidth(.5),
},
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: [
TableRow(
children: [
getRowTitle('Country'),
getRowValue(
'${data.location.country}',
),
],
),
TableRow(
children: [
getRowTitle('City'),
getRowValue(data.location.city),
],
),
TableRow(
children: [
getRowTitle('Description'),
getRowValue(data.location.description),
],
),
],
),
// BrandText.body1('providers.server.bottom_sheet.2'.tr()),
// SizedBox(height: 10),
// BrandText.body1('providers.server.bottom_sheet.3'.tr()),
],
),
),
],
);
} else {
throw Exception('wrong state');
}
}),
),
_ServerSettings(tabController: tabController),
],
);
}
Widget getRowTitle(String title) {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: BrandText.h5(
title,
textAlign: TextAlign.right,
),
);
}
Widget getRowValue(String title, {bool isBold = false}) {
return BrandText.body1(
title,
style: isBold
? TextStyle(
fontWeight: NamedFontWeight.demiBold,
)
: null,
);
}
}
enum _PopupMenuItemType { setting }
class _TempMessage extends StatelessWidget {
const _TempMessage({
Key? key,
required this.message,
}) : super(key: key);
final String message;
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height - 100,
child: Center(
child: BrandText.body2(message),
),
);
}
}
final DateFormat formater = DateFormat('HH:mm:ss');

136
lib/ui/pages/server_details/server_settings.dart

@ -0,0 +1,136 @@
part of 'server_details.dart';
class _ServerSettings extends StatelessWidget {
const _ServerSettings({
Key? key,
required this.tabController,
}) : super(key: key);
final TabController tabController;
@override
Widget build(BuildContext context) {
return ListView(
padding: brandPagePadding2,
children: [
SizedBox(height: 10),
Container(
height: 52,
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 1),
child: Container(
child: Row(
children: [
IconButton(
icon: Icon(BrandIcons.arrow_left),
onPressed: () => tabController.animateTo(0),
),
SizedBox(width: 10),
BrandText.h4('basis.settings'.tr()),
],
),
),
),
BrandDivider(),
SwitcherBlock(
onChange: (_) {},
child: _TextColumn(
title: 'Allow Auto-upgrade',
value: 'Wether to allow automatic packages upgrades',
),
isActive: true,
),
SwitcherBlock(
onChange: (_) {},
child: _TextColumn(
title: 'Reboot after upgrade',
value: 'Reboot without prompt after applying updates',
),
isActive: false,
),
_Button(
onTap: () {},
child: _TextColumn(
title: 'Server Timezone',
value: 'Europe/Kyiv',
),
),
_Button(
onTap: () {},
child: _TextColumn(
title: 'Server Locale',
value: 'Default',
),
),
_Button(
onTap: () {},
child: _TextColumn(
hasWarning: true,
title: 'Factory Reset',
value: 'Restore default settings on your server',
),
)
],
);
}
}
class _Button extends StatelessWidget {
const _Button({
Key? key,
required this.onTap,
required this.child,
}) : super(key: key);
final Widget child;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.only(top: 20, bottom: 5),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: BrandColors.dividerColor),
)),
child: child,
),
);
}
}
class _TextColumn extends StatelessWidget {
const _TextColumn({
Key? key,
required this.title,
required this.value,
this.hasWarning = false,
}) : super(key: key);
final String title;
final String value;
final bool hasWarning;
@